@webskill/sdk 0.2.3 → 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 +3 -2
- package/dist/browser.js +47 -15
- package/dist/{dist-NM4Mylx4.js → dist-B77plHjw.js} +230 -105
- package/dist/{dist-D0saNPi_.js → dist-BQzncxXg.js} +98 -5
- package/dist/{dist-CvIMVwr3.js → dist-Chk8iB-E.js} +101 -38
- package/dist/{dist-BJobG0i-.js → dist-DNSG9FqC.js} +108 -69
- package/dist/governance.d.ts +13 -5
- package/dist/governance.js +50 -20
- package/dist/{index-iqcz3_NS.d.ts → index-CsDJvYGV.d.ts} +8 -3
- package/dist/{index-gjFuBevI.d.ts → index-DfINBEOy.d.ts} +29 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +7 -3
- package/dist/mcp.js +35 -18
- package/dist/node.d.ts +3 -3
- package/dist/node.js +3 -3
- package/dist/processSandboxEntry.js +11 -0
- package/dist/sandboxWorkerEntry.js +11 -0
- package/dist/{testing-B4pq6JYa.js → testing-BUoXvm1u.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CKm5G_eQ-8W8FnP4u.d.ts → types-CKm5G_eQ-krKWW8WV.d.ts} +40 -7
- 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) */
|
|
@@ -111,6 +115,18 @@ var MemoryFS = class {
|
|
|
111
115
|
async writeText(path, content) {
|
|
112
116
|
await this.writeBinary(path, new TextEncoder().encode(content));
|
|
113
117
|
}
|
|
118
|
+
async appendText(path, content) {
|
|
119
|
+
const key = this.#normalize(path);
|
|
120
|
+
const existing = this.#entries.get(key);
|
|
121
|
+
if (existing && existing.type !== "file") throw new WebSkillError("FS_PERMISSION_DENIED", `Cannot append to a directory: ${key}`);
|
|
122
|
+
const prior = existing ? new TextDecoder().decode(existing.content) : "";
|
|
123
|
+
this.#ensureParents(key);
|
|
124
|
+
this.#entries.set(key, {
|
|
125
|
+
type: "file",
|
|
126
|
+
content: new TextEncoder().encode(prior + content),
|
|
127
|
+
mtimeMs: Date.now()
|
|
128
|
+
});
|
|
129
|
+
}
|
|
114
130
|
async readBinary(path) {
|
|
115
131
|
return this.#getFile(this.#normalize(path)).content;
|
|
116
132
|
}
|
|
@@ -188,7 +204,12 @@ var MemoryFS = class {
|
|
|
188
204
|
this.#entries.delete(src);
|
|
189
205
|
}
|
|
190
206
|
};
|
|
191
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* 原子写文本:先写临时文件再 rename(进程崩溃不留下半写文件;lockfile 等账本场景)。
|
|
209
|
+
* 原子性强弱取决于 provider 的 rename 实现:NodeFS/MemoryFS 为真 rename;
|
|
210
|
+
* OPFS 无原生 move,rename 是 copy+delete(非原子,崩溃可能残留双份),
|
|
211
|
+
* 对崩溃一致性要求极高的场景在浏览器侧需知悉此前提。
|
|
212
|
+
*/
|
|
192
213
|
async function atomicWriteText(fs, path, content) {
|
|
193
214
|
const tmp = `${path}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
194
215
|
await fs.writeText(tmp, content);
|
|
@@ -203,7 +224,9 @@ function normalizePath(path) {
|
|
|
203
224
|
* 拒绝空串、`.`、`..`、含 `/` 或 `\`、含 `:`(Windows 盘符/ADS)。
|
|
204
225
|
* 违规抛 FS_PATH_OUTSIDE_ROOT(kind 用于错误消息定位,如 "runId")。
|
|
205
226
|
*/
|
|
227
|
+
const CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/;
|
|
206
228
|
function assertSafePathSegment(segment, kind) {
|
|
229
|
+
if (CONTROL_CHARS_RE.test(segment)) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Invalid ${kind} (control characters including NUL are not allowed): ${JSON.stringify(segment)}`);
|
|
207
230
|
if (segment === "" || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\") || segment.includes(":")) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Invalid ${kind} (must be a single safe path segment): ${JSON.stringify(segment)}`);
|
|
208
231
|
}
|
|
209
232
|
/**
|
|
@@ -221,6 +244,7 @@ function resolveInsideRoot(root, relativePath) {
|
|
|
221
244
|
if (relativePath.trim() === "") fail("empty path");
|
|
222
245
|
if (relativePath.startsWith("/") || relativePath.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(relativePath)) fail("absolute paths not allowed");
|
|
223
246
|
const segments = relativePath.replace(/\\/g, "/").split("/");
|
|
247
|
+
if (CONTROL_CHARS_RE.test(relativePath)) fail("control characters including NUL are not allowed");
|
|
224
248
|
if (segments.some((s) => s === "..")) fail("`..` segments not allowed");
|
|
225
249
|
const clean = segments.filter((s) => s !== "" && s !== ".");
|
|
226
250
|
if (clean.length === 0) fail("path resolves to empty");
|
|
@@ -228,6 +252,65 @@ function resolveInsideRoot(root, relativePath) {
|
|
|
228
252
|
const joined = normalizedRoot === "" ? clean.join("/") : `${normalizedRoot}/${clean.join("/")}`;
|
|
229
253
|
return root.startsWith("/") || root.startsWith("\\") ? `/${joined}` : joined;
|
|
230
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
|
+
}
|
|
231
314
|
const FRONTMATTER_RE = /^---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---[^\S\r\n]*(?:\r?\n|$)([\s\S]*)$/;
|
|
232
315
|
const invalid = (message, details) => {
|
|
233
316
|
throw new WebSkillError("SKILL_INVALID_METADATA", message, details);
|
|
@@ -368,7 +451,6 @@ function checkDependencyCycles(adjacency) {
|
|
|
368
451
|
*/
|
|
369
452
|
/** 包级清单文件名(位于 zip 根,不参与各技能 digest) */
|
|
370
453
|
const SKILL_PACK_FILE = "webskill.skill-pack.json";
|
|
371
|
-
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
372
454
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
373
455
|
async function listFilesRecursive(fs, root, prefix = "") {
|
|
374
456
|
const out = [];
|
|
@@ -392,6 +474,7 @@ async function exportSkills(fs, input) {
|
|
|
392
474
|
for (const root of input.roots) {
|
|
393
475
|
const base = root.replace(/\/+$/, "");
|
|
394
476
|
const manifest = await input.manifestBuilder(base);
|
|
477
|
+
if (!isValidSkillName(manifest.name)) throw new WebSkillError("EXPORT_FAILED", `Skill "${manifest.name}" has an invalid name and cannot be exported to a skill pack`);
|
|
395
478
|
skills.push({
|
|
396
479
|
name: manifest.name,
|
|
397
480
|
digest: manifest.integrity.digest
|
|
@@ -427,7 +510,7 @@ function parseSkillPackManifest(text) {
|
|
|
427
510
|
for (const entry of record["skills"]) {
|
|
428
511
|
if (typeof entry !== "object" || entry === null) invalid("skill entry is not an object");
|
|
429
512
|
const { name, digest } = entry;
|
|
430
|
-
if (typeof name !== "string" || name === "") invalid(
|
|
513
|
+
if (typeof name !== "string" || name === "" || !isValidSkillName(name)) invalid(`skill entry has an invalid name: ${JSON.stringify(name)}`);
|
|
431
514
|
if (typeof digest !== "string" || digest === "") invalid(`skill "${name}" missing a valid "digest"`);
|
|
432
515
|
}
|
|
433
516
|
return raw;
|
|
@@ -468,6 +551,14 @@ async function unzipWithLimits(data, limits) {
|
|
|
468
551
|
const unzip = new Unzip();
|
|
469
552
|
unzip.register(UnzipInflate);
|
|
470
553
|
unzip.onfile = (file) => {
|
|
554
|
+
if (file.name.startsWith("/") || file.name.startsWith("\\") || /^[a-zA-Z]:[\\/]/.test(file.name)) {
|
|
555
|
+
failure = new WebSkillError("INSTALL_FAILED", `Archive entry escapes the destination root: ${file.name}`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (file.name.replace(/\\/g, "/").split("/").some((s) => s === "..") || /[\x00-\x1f\x7f]/.test(file.name)) {
|
|
559
|
+
failure = new WebSkillError("INSTALL_FAILED", `Archive entry escapes the destination root: ${file.name}`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
471
562
|
if (file.name.endsWith("/")) {
|
|
472
563
|
entries.push([file.name, /* @__PURE__ */ new Uint8Array(0)]);
|
|
473
564
|
return;
|
|
@@ -579,7 +670,7 @@ var SkillDiscovery = class {
|
|
|
579
670
|
const candidates = [];
|
|
580
671
|
const knownSkillNames = /* @__PURE__ */ new Set();
|
|
581
672
|
const adjacency = /* @__PURE__ */ new Map();
|
|
582
|
-
for (const root of this.#roots) {
|
|
673
|
+
for (const [rootIndex, root] of this.#roots.entries()) {
|
|
583
674
|
if (!await this.#fs.exists(root)) {
|
|
584
675
|
issues.push({
|
|
585
676
|
code: "FS_NOT_FOUND",
|
|
@@ -604,6 +695,7 @@ var SkillDiscovery = class {
|
|
|
604
695
|
candidates.push({
|
|
605
696
|
dirName,
|
|
606
697
|
skillRoot,
|
|
698
|
+
rootIndex,
|
|
607
699
|
hasSkillMd,
|
|
608
700
|
metadata,
|
|
609
701
|
parseError,
|
|
@@ -616,6 +708,7 @@ var SkillDiscovery = class {
|
|
|
616
708
|
}
|
|
617
709
|
}));
|
|
618
710
|
}
|
|
711
|
+
candidates.sort((a, b) => a.rootIndex - b.rootIndex || a.dirName.localeCompare(b.dirName));
|
|
619
712
|
const cycles = checkDependencyCycles(adjacency);
|
|
620
713
|
const claimedNames = /* @__PURE__ */ new Set();
|
|
621
714
|
for (const candidate of candidates) {
|
|
@@ -743,4 +836,4 @@ const xmlRenderer = {
|
|
|
743
836
|
};
|
|
744
837
|
|
|
745
838
|
//#endregion
|
|
746
|
-
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,
|
|
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";
|
|
@@ -37,13 +37,16 @@ async function realpathNearest(platformPath) {
|
|
|
37
37
|
* 可选 root 模式:构造传入 root 后,read/write 操作先做 realpath 包含校验
|
|
38
38
|
* (root 与目标都 realpath 后前缀比对),经符号链接逃逸 root → FS_PATH_OUTSIDE_ROOT。
|
|
39
39
|
*/
|
|
40
|
-
var NodeFS = class {
|
|
40
|
+
var NodeFS = class NodeFS {
|
|
41
41
|
kind = "node";
|
|
42
42
|
#root;
|
|
43
43
|
constructor(deps = {}) {
|
|
44
44
|
this.#root = deps.root;
|
|
45
45
|
}
|
|
46
|
-
/** root
|
|
46
|
+
/** root 模式:全部方法(read/write/exists/stat/list/mkdir/remove/rename)目标 realpath 必须落在 root realpath 前缀内 */
|
|
47
|
+
withRoot(root) {
|
|
48
|
+
return new NodeFS({ root });
|
|
49
|
+
}
|
|
47
50
|
async #assertContained(p) {
|
|
48
51
|
if (!this.#root) return;
|
|
49
52
|
const rootReal = await promises.realpath(toPlatform$4(this.#root));
|
|
@@ -64,6 +67,12 @@ var NodeFS = class {
|
|
|
64
67
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
65
68
|
await promises.writeFile(target, content, "utf8");
|
|
66
69
|
}
|
|
70
|
+
async appendText(p, content) {
|
|
71
|
+
await this.#assertContained(p);
|
|
72
|
+
const target = toPlatform$4(p);
|
|
73
|
+
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
74
|
+
await promises.appendFile(target, content, "utf8");
|
|
75
|
+
}
|
|
67
76
|
async readBinary(p) {
|
|
68
77
|
await this.#assertContained(p);
|
|
69
78
|
try {
|
|
@@ -79,6 +88,7 @@ var NodeFS = class {
|
|
|
79
88
|
await promises.writeFile(target, content);
|
|
80
89
|
}
|
|
81
90
|
async exists(p) {
|
|
91
|
+
await this.#assertContained(p);
|
|
82
92
|
try {
|
|
83
93
|
await promises.access(toPlatform$4(p));
|
|
84
94
|
return true;
|
|
@@ -87,6 +97,7 @@ var NodeFS = class {
|
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
async stat(p) {
|
|
100
|
+
await this.#assertContained(p);
|
|
90
101
|
try {
|
|
91
102
|
const s = await promises.stat(toPlatform$4(p));
|
|
92
103
|
return {
|
|
@@ -100,6 +111,7 @@ var NodeFS = class {
|
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
async list(p) {
|
|
114
|
+
await this.#assertContained(p);
|
|
103
115
|
let dirents;
|
|
104
116
|
try {
|
|
105
117
|
dirents = await promises.readdir(toPlatform$4(p), { withFileTypes: true });
|
|
@@ -112,9 +124,11 @@ var NodeFS = class {
|
|
|
112
124
|
}));
|
|
113
125
|
}
|
|
114
126
|
async mkdir(p) {
|
|
127
|
+
await this.#assertContained(p);
|
|
115
128
|
await promises.mkdir(toPlatform$4(p), { recursive: true });
|
|
116
129
|
}
|
|
117
130
|
async remove(p, options) {
|
|
131
|
+
await this.#assertContained(p);
|
|
118
132
|
try {
|
|
119
133
|
await promises.rm(toPlatform$4(p), { recursive: options?.recursive ?? false });
|
|
120
134
|
} catch (e) {
|
|
@@ -122,6 +136,8 @@ var NodeFS = class {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
async rename(from, to) {
|
|
139
|
+
await this.#assertContained(from);
|
|
140
|
+
await this.#assertContained(to);
|
|
125
141
|
await promises.mkdir(path.dirname(toPlatform$4(to)), { recursive: true });
|
|
126
142
|
try {
|
|
127
143
|
await promises.rename(toPlatform$4(from), toPlatform$4(to));
|
|
@@ -279,7 +295,6 @@ var NodeScriptExecutor = class {
|
|
|
279
295
|
};
|
|
280
296
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
281
297
|
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
282
|
-
const messageOf$6 = (e) => e instanceof Error ? e.message : String(e);
|
|
283
298
|
/**
|
|
284
299
|
* 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
|
|
285
300
|
* runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
|
|
@@ -365,7 +380,7 @@ var SandboxedScriptExecutor = class {
|
|
|
365
380
|
content: [],
|
|
366
381
|
error: {
|
|
367
382
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
368
|
-
message: messageOf
|
|
383
|
+
message: messageOf(e)
|
|
369
384
|
}
|
|
370
385
|
};
|
|
371
386
|
}
|
|
@@ -381,12 +396,13 @@ var SandboxedScriptExecutor = class {
|
|
|
381
396
|
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
382
397
|
if (!result.ok) {
|
|
383
398
|
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
399
|
+
const normalized = normalizeToolError(result.error?.code, result.error?.message ?? "script execution failed");
|
|
384
400
|
return {
|
|
385
401
|
ok: false,
|
|
386
402
|
content: [],
|
|
387
403
|
error: {
|
|
388
|
-
code:
|
|
389
|
-
message: `${
|
|
404
|
+
code: normalized.code,
|
|
405
|
+
message: `${normalized.message}${stderrSummary}`
|
|
390
406
|
}
|
|
391
407
|
};
|
|
392
408
|
}
|
|
@@ -405,7 +421,7 @@ var SandboxedScriptExecutor = class {
|
|
|
405
421
|
content: [],
|
|
406
422
|
error: {
|
|
407
423
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
408
|
-
message: messageOf
|
|
424
|
+
message: messageOf(e)
|
|
409
425
|
}
|
|
410
426
|
};
|
|
411
427
|
}
|
|
@@ -456,14 +472,14 @@ var SandboxedScriptExecutor = class {
|
|
|
456
472
|
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
457
473
|
return;
|
|
458
474
|
}
|
|
459
|
-
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))));
|
|
460
476
|
return;
|
|
461
477
|
}
|
|
462
478
|
if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
|
|
463
479
|
});
|
|
464
480
|
worker.on("error", (e) => done(() => reject(e)));
|
|
465
481
|
worker.on("exit", (code) => {
|
|
466
|
-
if (
|
|
482
|
+
if (!settled) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker exited ${code === null ? "by signal" : `with code ${code}`} without producing a result`)));
|
|
467
483
|
});
|
|
468
484
|
});
|
|
469
485
|
}
|
|
@@ -517,13 +533,12 @@ var SandboxedScriptExecutor = class {
|
|
|
517
533
|
}
|
|
518
534
|
}
|
|
519
535
|
} catch (e) {
|
|
520
|
-
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));
|
|
521
537
|
}
|
|
522
538
|
}
|
|
523
539
|
};
|
|
524
540
|
const baseName = (p) => p.split("/").pop() ?? p;
|
|
525
541
|
const toPlatform$2 = (p) => p.split("/").join(path.sep);
|
|
526
|
-
const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
|
|
527
542
|
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
528
543
|
const readAllow = (p) => [`--allow-fs-read=${p}`, `--allow-fs-read=${realpathSync(p)}`];
|
|
529
544
|
const writeAllow = (p) => [`--allow-fs-write=${p}`, `--allow-fs-write=${realpathSync(p)}`];
|
|
@@ -555,6 +570,7 @@ var ProcessSandboxExecutor = class {
|
|
|
555
570
|
#approval;
|
|
556
571
|
#slots = [];
|
|
557
572
|
#waiters = [];
|
|
573
|
+
#disposed = false;
|
|
558
574
|
constructor(fs, options = {}) {
|
|
559
575
|
this.#fs = fs;
|
|
560
576
|
this.#options = options;
|
|
@@ -572,8 +588,9 @@ var ProcessSandboxExecutor = class {
|
|
|
572
588
|
get poolSize() {
|
|
573
589
|
return this.#options.poolSize ?? 2;
|
|
574
590
|
}
|
|
575
|
-
/**
|
|
591
|
+
/** 池全部子进程销毁(测试收尾/进程退出前调用);排队中的 acquire 一律 reject(不悬挂) */
|
|
576
592
|
async dispose() {
|
|
593
|
+
this.#disposed = true;
|
|
577
594
|
for (const slot of this.#slots) {
|
|
578
595
|
slot.child.kill();
|
|
579
596
|
await rm(slot.artifactDir, {
|
|
@@ -582,7 +599,8 @@ var ProcessSandboxExecutor = class {
|
|
|
582
599
|
}).catch(() => void 0);
|
|
583
600
|
}
|
|
584
601
|
this.#slots = [];
|
|
585
|
-
|
|
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"));
|
|
586
604
|
}
|
|
587
605
|
async #locateScript(skillRoot, scriptName) {
|
|
588
606
|
const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
|
|
@@ -595,6 +613,7 @@ var ProcessSandboxExecutor = class {
|
|
|
595
613
|
/** 取一个可用子进程(同 key 复用 / 淘汰 idle 重生 / 扩容 / 排队) */
|
|
596
614
|
async #acquire(key) {
|
|
597
615
|
for (;;) {
|
|
616
|
+
if (this.#disposed) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Process sandbox executor is disposed");
|
|
598
617
|
const idle = this.#slots.filter((s) => !s.busy);
|
|
599
618
|
const match = idle.find((s) => s.key === key);
|
|
600
619
|
if (match) {
|
|
@@ -616,7 +635,10 @@ var ProcessSandboxExecutor = class {
|
|
|
616
635
|
this.#slots.push(slot);
|
|
617
636
|
return slot;
|
|
618
637
|
}
|
|
619
|
-
await new Promise((resolve) => this.#waiters.push(
|
|
638
|
+
await new Promise((resolve, reject) => this.#waiters.push({
|
|
639
|
+
resolve,
|
|
640
|
+
reject
|
|
641
|
+
}));
|
|
620
642
|
}
|
|
621
643
|
}
|
|
622
644
|
/** 执行后回收:kill 旧子进程,补位重生同 key 新子进程(温池保持),唤醒排队 */
|
|
@@ -628,13 +650,27 @@ var ProcessSandboxExecutor = class {
|
|
|
628
650
|
recursive: true,
|
|
629
651
|
force: true
|
|
630
652
|
}).catch(() => void 0);
|
|
631
|
-
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
|
+
}
|
|
632
662
|
this.#slots.push(fresh);
|
|
633
|
-
}).catch(() =>
|
|
663
|
+
}).catch((e) => {
|
|
664
|
+
this.#warn(`Failed to respawn sandbox child for pool maintenance: ${messageOf(e)}`);
|
|
665
|
+
}).finally(() => {
|
|
634
666
|
const waiter = this.#waiters.shift();
|
|
635
|
-
if (waiter) waiter();
|
|
667
|
+
if (waiter) waiter.resolve();
|
|
636
668
|
});
|
|
637
669
|
}
|
|
670
|
+
#warn(message) {
|
|
671
|
+
if (this.#options.onWarning) this.#options.onWarning(message);
|
|
672
|
+
else console.warn(message);
|
|
673
|
+
}
|
|
638
674
|
async #spawnSlot(key) {
|
|
639
675
|
const artifactDir = await mkdtemp(path.join(tmpdir(), "webskill-psbx-out-")).then((d) => d.split(path.sep).join("/"));
|
|
640
676
|
const entry = processEntryPath();
|
|
@@ -695,7 +731,7 @@ var ProcessSandboxExecutor = class {
|
|
|
695
731
|
content: [],
|
|
696
732
|
error: {
|
|
697
733
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
698
|
-
message: messageOf
|
|
734
|
+
message: messageOf(e)
|
|
699
735
|
}
|
|
700
736
|
};
|
|
701
737
|
}
|
|
@@ -713,12 +749,13 @@ var ProcessSandboxExecutor = class {
|
|
|
713
749
|
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
714
750
|
if (!result.ok) {
|
|
715
751
|
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
752
|
+
const normalized = normalizeToolError(result.error?.code, result.error?.message ?? "script execution failed");
|
|
716
753
|
return {
|
|
717
754
|
ok: false,
|
|
718
755
|
content: [],
|
|
719
756
|
error: {
|
|
720
|
-
code:
|
|
721
|
-
message: `${
|
|
757
|
+
code: normalized.code,
|
|
758
|
+
message: `${normalized.message}${stderrSummary}`
|
|
722
759
|
}
|
|
723
760
|
};
|
|
724
761
|
}
|
|
@@ -737,7 +774,7 @@ var ProcessSandboxExecutor = class {
|
|
|
737
774
|
content: [],
|
|
738
775
|
error: {
|
|
739
776
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
740
|
-
message: messageOf
|
|
777
|
+
message: messageOf(e)
|
|
741
778
|
}
|
|
742
779
|
};
|
|
743
780
|
} finally {
|
|
@@ -776,7 +813,7 @@ var ProcessSandboxExecutor = class {
|
|
|
776
813
|
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
777
814
|
return;
|
|
778
815
|
}
|
|
779
|
-
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))));
|
|
780
817
|
return;
|
|
781
818
|
}
|
|
782
819
|
if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
|
|
@@ -845,7 +882,7 @@ var ProcessSandboxExecutor = class {
|
|
|
845
882
|
}
|
|
846
883
|
}
|
|
847
884
|
} catch (e) {
|
|
848
|
-
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));
|
|
849
886
|
}
|
|
850
887
|
}
|
|
851
888
|
};
|
|
@@ -1177,7 +1214,6 @@ var CliUiBridge = class {
|
|
|
1177
1214
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
1178
1215
|
}
|
|
1179
1216
|
};
|
|
1180
|
-
const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
1181
1217
|
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
1182
1218
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
1183
1219
|
let tarModule$1;
|
|
@@ -1226,7 +1262,7 @@ async function extractTarFile(archivePath, destRoot, limits) {
|
|
|
1226
1262
|
});
|
|
1227
1263
|
} catch (e) {
|
|
1228
1264
|
if (e instanceof WebSkillError) throw e;
|
|
1229
|
-
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);
|
|
1230
1266
|
}
|
|
1231
1267
|
}
|
|
1232
1268
|
/** 递归列出目录内全部文件(相对路径,posix 分隔,不含目录条目) */
|
|
@@ -1270,7 +1306,6 @@ async function removeDirQuiet(fs, dir) {
|
|
|
1270
1306
|
if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
|
|
1271
1307
|
} catch {}
|
|
1272
1308
|
}
|
|
1273
|
-
const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
|
|
1274
1309
|
let tarModule;
|
|
1275
1310
|
async function loadTar() {
|
|
1276
1311
|
tarModule ??= await import("tar").catch((e) => {
|
|
@@ -1292,7 +1327,7 @@ async function exportArchive(fs, skillRoot, options) {
|
|
|
1292
1327
|
return options.outPath;
|
|
1293
1328
|
} catch (e) {
|
|
1294
1329
|
if (e instanceof WebSkillError) throw e;
|
|
1295
|
-
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);
|
|
1296
1331
|
}
|
|
1297
1332
|
}
|
|
1298
1333
|
/** 只解出归档中的 webskill.skill-manifest.json 条目(安装前预览) */
|
|
@@ -1320,13 +1355,12 @@ async function readArchiveManifest(fs, archivePath) {
|
|
|
1320
1355
|
}
|
|
1321
1356
|
});
|
|
1322
1357
|
} catch (e) {
|
|
1323
|
-
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);
|
|
1324
1359
|
}
|
|
1325
1360
|
if (manifestText === void 0) throw notFound();
|
|
1326
1361
|
return JSON.parse(manifestText);
|
|
1327
1362
|
}
|
|
1328
1363
|
const _execFileP = promisify(execFile);
|
|
1329
|
-
const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
1330
1364
|
/**
|
|
1331
1365
|
* 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
|
|
1332
1366
|
* git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
|
|
@@ -1342,7 +1376,7 @@ async function execFileP(command, args, options = {}) {
|
|
|
1342
1376
|
}
|
|
1343
1377
|
/** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
|
|
1344
1378
|
function commandFailed(command, e) {
|
|
1345
|
-
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);
|
|
1346
1380
|
}
|
|
1347
1381
|
/** git url 协议白名单:https:// 与 git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
|
|
1348
1382
|
* 其余显式协议(ext::/file:// 等)一律拒绝 */
|
|
@@ -1391,15 +1425,38 @@ async function stageGit(source, ctx) {
|
|
|
1391
1425
|
function sha256Hex(data) {
|
|
1392
1426
|
return createHash("sha256").update(data).digest("hex");
|
|
1393
1427
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
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 校验包体)→ 解包 → 定位技能根/包集 */
|
|
1396
1452
|
async function stageHttp(source, ctx, expectedSha256) {
|
|
1397
1453
|
const fetchImpl = ctx.fetchImpl ?? fetch;
|
|
1398
1454
|
let res;
|
|
1399
1455
|
try {
|
|
1400
|
-
res = await
|
|
1456
|
+
res = await fetchWithSsrfGuard(source, fetchImpl);
|
|
1401
1457
|
} catch (e) {
|
|
1402
|
-
|
|
1458
|
+
if (e instanceof WebSkillError) throw e;
|
|
1459
|
+
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf(e)}`, e);
|
|
1403
1460
|
}
|
|
1404
1461
|
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
1405
1462
|
const data = await readResponseWithLimit(res, ctx.archiveLimits);
|
|
@@ -1448,13 +1505,19 @@ async function stageLocal(source, ctx) {
|
|
|
1448
1505
|
/** npm 包名(@scope/name 或 name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
|
|
1449
1506
|
const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
1450
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._~+@/\\:-]+$/;
|
|
1451
1513
|
function assertNpmSpecSafe(source) {
|
|
1452
1514
|
const name = source.packageName;
|
|
1453
1515
|
const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
|
|
1454
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)}`);
|
|
1455
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)}`);
|
|
1456
1519
|
}
|
|
1457
|
-
/** npm 源:npm pack → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
|
|
1520
|
+
/** npm 源:npm pack --ignore-scripts → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
|
|
1458
1521
|
async function stageNpm(source, ctx) {
|
|
1459
1522
|
assertNpmSpecSafe(source);
|
|
1460
1523
|
const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
|
|
@@ -1462,6 +1525,7 @@ async function stageNpm(source, ctx) {
|
|
|
1462
1525
|
try {
|
|
1463
1526
|
({stdout} = await execFileP("npm", [
|
|
1464
1527
|
"pack",
|
|
1528
|
+
"--ignore-scripts",
|
|
1465
1529
|
"--pack-destination",
|
|
1466
1530
|
ctx.stagingRoot,
|
|
1467
1531
|
"--",
|
|
@@ -1578,7 +1642,6 @@ async function verifyIntegrity(fs, skillRoot) {
|
|
|
1578
1642
|
for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
|
|
1579
1643
|
return verifyManifest(manifest, actualHashes, actualFiles);
|
|
1580
1644
|
}
|
|
1581
|
-
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
1582
1645
|
const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
|
|
1583
1646
|
/**
|
|
1584
1647
|
* 技能管理门面:统一安装管线(staging → 解析 name → 校验 → 拷贝 → manifest → lockfile),
|