@webskill/sdk 0.1.3 → 0.1.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/README.md +5 -2
- package/dist/browser.d.ts +8 -4
- package/dist/browser.js +69 -39
- package/dist/{dist-DxGyAznR.js → dist-CfBOjJ4p.js} +46 -3
- package/dist/{dist-DZobLFh6.js → dist-DuGN5x0v.js} +123 -20
- package/dist/{dist-Cm_j6Jol.js → dist-LIFS3ZjI.js} +204 -74
- package/dist/{dist-DS1sfgHa.js → dist-fZFZaf43.js} +145 -7
- package/dist/governance.d.ts +13 -4
- package/dist/governance.js +35 -5
- package/dist/{index-CPuwsnmB.d.ts → index-Bun1aEf4.d.ts} +20 -3
- package/dist/{index-CqMuvcb2.d.ts → index-COuOu6gw.d.ts} +19 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +9 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js +3 -3
- package/dist/sandboxWorkerEntry.d.ts +1 -0
- package/dist/sandboxWorkerEntry.js +231 -0
- package/dist/{testing-CbM6rJ-E.js → testing-BmR48Pn1.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CgNC-oQu-Dq_A1yA-.d.ts → types-CgNC-oQu-DEcIBJKG.d.ts} +39 -3
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +1 -1
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +2 -2
- package/package.json +1 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, c as FsArtifactStore, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, o as CapabilityApproval } from "./dist-
|
|
1
|
+
import { A as validateSkills, C as parseSkillPackManifest, D as resolveArchiveLimits, O as resolveInsideRoot, S as parseSkillMarkdown, i as SKILL_MANIFEST_FILE, j as verifyManifest, k as unzipWithLimits, p as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, v as exportSkills, w as readResponseWithLimit, y as isValidSkillName } from "./dist-fZFZaf43.js";
|
|
2
|
+
import { M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, c as FsArtifactStore, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, o as CapabilityApproval } from "./dist-DuGN5x0v.js";
|
|
3
3
|
import { unzipSync, zipSync } from "fflate";
|
|
4
4
|
import { existsSync, promises, readFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
6
7
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
import { format, promisify } from "node:util";
|
|
8
9
|
import { Worker } from "node:worker_threads";
|
|
9
10
|
import { parseSync } from "oxc-parser";
|
|
10
11
|
import { createInterface } from "node:readline/promises";
|
|
11
12
|
import { mkdtemp } from "node:fs/promises";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
13
|
import * as tar from "tar";
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
@@ -18,10 +18,41 @@ import { createHash } from "node:crypto";
|
|
|
18
18
|
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
19
19
|
const toPosix = (p) => p.split(path.sep).join("/");
|
|
20
20
|
const isEnoent = (e) => typeof e === "object" && e !== null && e.code === "ENOENT";
|
|
21
|
-
/**
|
|
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
|
+
*/
|
|
22
41
|
var NodeFS = class {
|
|
23
42
|
kind = "node";
|
|
43
|
+
#root;
|
|
44
|
+
constructor(deps = {}) {
|
|
45
|
+
this.#root = deps.root;
|
|
46
|
+
}
|
|
47
|
+
/** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
|
|
48
|
+
async #assertContained(p) {
|
|
49
|
+
if (!this.#root) return;
|
|
50
|
+
const rootReal = await promises.realpath(toPlatform$3(this.#root));
|
|
51
|
+
const targetReal = await realpathNearest(toPlatform$3(p));
|
|
52
|
+
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})`);
|
|
53
|
+
}
|
|
24
54
|
async readText(p) {
|
|
55
|
+
await this.#assertContained(p);
|
|
25
56
|
try {
|
|
26
57
|
return await promises.readFile(toPlatform$3(p), "utf8");
|
|
27
58
|
} catch (e) {
|
|
@@ -29,11 +60,13 @@ var NodeFS = class {
|
|
|
29
60
|
}
|
|
30
61
|
}
|
|
31
62
|
async writeText(p, content) {
|
|
63
|
+
await this.#assertContained(p);
|
|
32
64
|
const target = toPlatform$3(p);
|
|
33
65
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
34
66
|
await promises.writeFile(target, content, "utf8");
|
|
35
67
|
}
|
|
36
68
|
async readBinary(p) {
|
|
69
|
+
await this.#assertContained(p);
|
|
37
70
|
try {
|
|
38
71
|
return await promises.readFile(toPlatform$3(p));
|
|
39
72
|
} catch (e) {
|
|
@@ -41,6 +74,7 @@ var NodeFS = class {
|
|
|
41
74
|
}
|
|
42
75
|
}
|
|
43
76
|
async writeBinary(p, content) {
|
|
77
|
+
await this.#assertContained(p);
|
|
44
78
|
const target = toPlatform$3(p);
|
|
45
79
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
46
80
|
await promises.writeFile(target, content);
|
|
@@ -94,7 +128,6 @@ var NodeFS = class {
|
|
|
94
128
|
}
|
|
95
129
|
};
|
|
96
130
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
97
|
-
const toPlatform$2 = (p) => p.split("/").join(path.sep);
|
|
98
131
|
/**
|
|
99
132
|
* 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
|
|
100
133
|
* worker_threads 沙箱见 deferred-items D1。
|
|
@@ -114,7 +147,22 @@ var NodeScriptExecutor = class {
|
|
|
114
147
|
return hasTs ? tsPath : jsPath;
|
|
115
148
|
}
|
|
116
149
|
async #loadModule(scriptPath) {
|
|
117
|
-
|
|
150
|
+
if (scriptPath.endsWith(".ts")) {
|
|
151
|
+
const source = await this.#fs.readText(scriptPath);
|
|
152
|
+
const dir = await promises.mkdtemp(path.join(tmpdir(), "webskill-ts-"));
|
|
153
|
+
try {
|
|
154
|
+
const file = path.join(dir, "script.ts");
|
|
155
|
+
await promises.writeFile(file, source);
|
|
156
|
+
return await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
|
|
157
|
+
} finally {
|
|
158
|
+
await promises.rm(dir, {
|
|
159
|
+
recursive: true,
|
|
160
|
+
force: true
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const source = await this.#fs.readText(scriptPath);
|
|
165
|
+
return await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(`${source}\n//# ${Date.now()}`)}`);
|
|
118
166
|
}
|
|
119
167
|
async loadDefinition(skillRoot, scriptName) {
|
|
120
168
|
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
@@ -223,14 +271,14 @@ var NodeScriptExecutor = class {
|
|
|
223
271
|
}
|
|
224
272
|
};
|
|
225
273
|
const baseName = (p) => p.split("/").pop() ?? p;
|
|
226
|
-
const toPlatform$
|
|
274
|
+
const toPlatform$2 = (p) => p.split("/").join(path.sep);
|
|
227
275
|
const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
|
|
228
276
|
/**
|
|
229
277
|
* 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
|
|
230
278
|
* runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
|
|
231
279
|
*/
|
|
232
280
|
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
233
|
-
/** Worker 入口文件:src 为同目录 .ts;dist 为 dist/executor
|
|
281
|
+
/** Worker 入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
|
|
234
282
|
function workerEntryPath() {
|
|
235
283
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
236
284
|
const candidates = [
|
|
@@ -239,13 +287,17 @@ function workerEntryPath() {
|
|
|
239
287
|
path.join(dir, "executor", "sandboxWorkerEntry.js")
|
|
240
288
|
];
|
|
241
289
|
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
242
|
-
|
|
290
|
+
throw new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker entry not found (tried: ${candidates.join(", ")})`);
|
|
243
291
|
}
|
|
244
292
|
/**
|
|
245
293
|
* D1:worker_threads 沙箱脚本执行器。
|
|
246
294
|
* 每次执行独立 Worker(resourceLimits + env 白名单),loadDefinition 同样在
|
|
247
295
|
* Worker 内完成(禁止主进程 import 不可信脚本);超时 terminate(可杀同步死循环)。
|
|
248
296
|
* 能力桥协议与浏览器同一来源(runtime/sandbox/bridgeProtocol)。
|
|
297
|
+
*
|
|
298
|
+
* 诚实标注:本执行器做的是**能力面收敛**(网络策略、模块 allowlist、资源限额、
|
|
299
|
+
* 超时强杀),**不是安全边界**——Worker 内脚本与宿主共享进程,仍有绕过手段;
|
|
300
|
+
* 禁止假定其可隔离不可信脚本。
|
|
249
301
|
*/
|
|
250
302
|
var SandboxedScriptExecutor = class {
|
|
251
303
|
#fs;
|
|
@@ -280,8 +332,9 @@ var SandboxedScriptExecutor = class {
|
|
|
280
332
|
const skillName = baseName(skillRoot);
|
|
281
333
|
const result = await this.#runWorker({
|
|
282
334
|
mode: "load",
|
|
283
|
-
scriptPath: toPlatform$
|
|
284
|
-
scriptName
|
|
335
|
+
scriptPath: toPlatform$2(scriptPath),
|
|
336
|
+
scriptName,
|
|
337
|
+
scriptSource: await this.#fs.readText(scriptPath)
|
|
285
338
|
});
|
|
286
339
|
if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
|
|
287
340
|
return {
|
|
@@ -310,11 +363,12 @@ var SandboxedScriptExecutor = class {
|
|
|
310
363
|
try {
|
|
311
364
|
const result = await this.#runWorker({
|
|
312
365
|
mode: "execute",
|
|
313
|
-
scriptPath: toPlatform$
|
|
366
|
+
scriptPath: toPlatform$2(scriptPath),
|
|
314
367
|
scriptName,
|
|
315
368
|
skillName: context.skillName,
|
|
316
369
|
runId: context.runId,
|
|
317
|
-
args
|
|
370
|
+
args,
|
|
371
|
+
scriptSource: await this.#fs.readText(scriptPath)
|
|
318
372
|
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
319
373
|
if (!result.ok) {
|
|
320
374
|
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
@@ -356,7 +410,8 @@ var SandboxedScriptExecutor = class {
|
|
|
356
410
|
workerData: {
|
|
357
411
|
...workerData,
|
|
358
412
|
networkPolicy: this.#networkPolicy,
|
|
359
|
-
networkPolicyLib: NETWORK_POLICY_LIB
|
|
413
|
+
networkPolicyLib: NETWORK_POLICY_LIB,
|
|
414
|
+
allowedModules: this.#options.allowedModules ?? []
|
|
360
415
|
},
|
|
361
416
|
env,
|
|
362
417
|
resourceLimits: this.#options.resourceLimits ?? {
|
|
@@ -776,42 +831,42 @@ var CliUiBridge = class {
|
|
|
776
831
|
}
|
|
777
832
|
};
|
|
778
833
|
const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
779
|
-
const toPlatform = (p) => p.split("/").join(path.sep);
|
|
834
|
+
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
780
835
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
781
836
|
/**
|
|
782
|
-
* zip 解包(fflate
|
|
837
|
+
* zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
|
|
783
838
|
* 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
|
|
784
839
|
*/
|
|
785
|
-
async function extractZipData(fs, data, destRoot) {
|
|
786
|
-
|
|
787
|
-
try {
|
|
788
|
-
entries = unzipSync(data);
|
|
789
|
-
} catch (e) {
|
|
790
|
-
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${messageOf$4(e)}`, e);
|
|
791
|
-
}
|
|
792
|
-
for (const [entryPath, content] of Object.entries(entries)) {
|
|
840
|
+
async function extractZipData(fs, data, destRoot, limits) {
|
|
841
|
+
for (const [entryPath, content] of await unzipWithLimits(data, limits)) {
|
|
793
842
|
const target = resolveInsideRoot(destRoot, entryPath);
|
|
794
843
|
if (entryPath.endsWith("/")) await fs.mkdir(target);
|
|
795
844
|
else await fs.writeBinary(target, content);
|
|
796
845
|
}
|
|
797
846
|
}
|
|
798
847
|
/**
|
|
799
|
-
* tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot
|
|
800
|
-
*
|
|
848
|
+
* tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot 校验并累计
|
|
849
|
+
* 单条目/总体积上限(声明 size 预检),再解包;符号链接/硬链接条目不跟随(跳过)。
|
|
801
850
|
*/
|
|
802
|
-
async function extractTarFile(archivePath, destRoot) {
|
|
851
|
+
async function extractTarFile(archivePath, destRoot, limits) {
|
|
852
|
+
const { maxEntryBytes, maxTotalBytes } = resolveArchiveLimits(limits);
|
|
803
853
|
try {
|
|
804
854
|
const entryPaths = [];
|
|
855
|
+
let total = 0;
|
|
805
856
|
await tar.t({
|
|
806
|
-
file: toPlatform(archivePath),
|
|
857
|
+
file: toPlatform$1(archivePath),
|
|
807
858
|
onentry: (entry) => {
|
|
808
859
|
entryPaths.push(entry.path);
|
|
860
|
+
const size = typeof entry.size === "number" ? entry.size : 0;
|
|
861
|
+
if (size > maxEntryBytes) throw new WebSkillError("INSTALL_FAILED", `Archive entry exceeds the ${maxEntryBytes}-byte limit: ${entry.path}`);
|
|
862
|
+
total += size;
|
|
863
|
+
if (total > maxTotalBytes) throw new WebSkillError("INSTALL_FAILED", `Archive contents exceed the ${maxTotalBytes}-byte total limit`);
|
|
809
864
|
}
|
|
810
865
|
});
|
|
811
866
|
for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
|
|
812
867
|
await tar.x({
|
|
813
|
-
file: toPlatform(archivePath),
|
|
814
|
-
cwd: toPlatform(destRoot),
|
|
868
|
+
file: toPlatform$1(archivePath),
|
|
869
|
+
cwd: toPlatform$1(destRoot),
|
|
815
870
|
filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
|
|
816
871
|
});
|
|
817
872
|
} catch (e) {
|
|
@@ -830,8 +885,23 @@ async function listFiles(fs, root, prefix = "") {
|
|
|
830
885
|
}
|
|
831
886
|
return out;
|
|
832
887
|
}
|
|
833
|
-
|
|
888
|
+
const toPlatform = (p) => p.split("/").join(path.sep);
|
|
889
|
+
/** lstat 扫描拒绝符号链接条目(与 tar 解包 filter 策略一致;虚拟 fs 路径不存在于真实 fs 时跳过) */
|
|
890
|
+
async function assertNoSymlinks(platformRoot) {
|
|
891
|
+
let entries;
|
|
892
|
+
try {
|
|
893
|
+
entries = await promises.readdir(platformRoot, { withFileTypes: true });
|
|
894
|
+
} catch {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
for (const entry of entries) {
|
|
898
|
+
if (entry.isSymbolicLink()) throw new WebSkillError("INSTALL_FAILED", `Refusing to copy symbolic link entry: ${platformRoot}${path.sep}${entry.name}`);
|
|
899
|
+
if (entry.isDirectory()) await assertNoSymlinks(`${platformRoot}${path.sep}${entry.name}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/** 递归拷贝目录(含子目录与二进制文件);真实 fs 上先做符号链接扫描 */
|
|
834
903
|
async function copyDir(fs, from, to) {
|
|
904
|
+
await assertNoSymlinks(toPlatform(from));
|
|
835
905
|
await fs.mkdir(to);
|
|
836
906
|
for (const entry of await fs.list(from)) {
|
|
837
907
|
const name = entry.path.split("/").pop() ?? "";
|
|
@@ -854,8 +924,8 @@ async function exportArchive(fs, skillRoot, options) {
|
|
|
854
924
|
for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
|
|
855
925
|
await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
|
|
856
926
|
} else await tar.c({
|
|
857
|
-
file: toPlatform(options.outPath),
|
|
858
|
-
cwd: toPlatform(skillRoot)
|
|
927
|
+
file: toPlatform$1(options.outPath),
|
|
928
|
+
cwd: toPlatform$1(skillRoot)
|
|
859
929
|
}, ["."]);
|
|
860
930
|
return options.outPath;
|
|
861
931
|
} catch (e) {
|
|
@@ -876,7 +946,7 @@ async function readArchiveManifest(fs, archivePath) {
|
|
|
876
946
|
let manifestText;
|
|
877
947
|
try {
|
|
878
948
|
await tar.t({
|
|
879
|
-
file: toPlatform(archivePath),
|
|
949
|
+
file: toPlatform$1(archivePath),
|
|
880
950
|
onentry: (entry) => {
|
|
881
951
|
if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
|
|
882
952
|
const chunks = [];
|
|
@@ -897,28 +967,39 @@ const _execFileP = promisify(execFile);
|
|
|
897
967
|
const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
898
968
|
/**
|
|
899
969
|
* 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
|
|
900
|
-
* git 等原生 exe
|
|
970
|
+
* git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
|
|
901
971
|
*/
|
|
902
|
-
async function execFileP(command, args) {
|
|
972
|
+
async function execFileP(command, args, options = {}) {
|
|
973
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
903
974
|
if (process.platform === "win32" && command === "npm") return _execFileP("cmd", [
|
|
904
975
|
"/c",
|
|
905
976
|
command,
|
|
906
977
|
...args
|
|
907
|
-
]);
|
|
908
|
-
return _execFileP(command, args);
|
|
978
|
+
], { timeout: timeoutMs });
|
|
979
|
+
return _execFileP(command, args, { timeout: timeoutMs });
|
|
909
980
|
}
|
|
910
981
|
/** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
|
|
911
982
|
function commandFailed(command, e) {
|
|
912
983
|
return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf$2(e)}`, e);
|
|
913
984
|
}
|
|
914
|
-
/** git
|
|
985
|
+
/** git url 协议白名单:https:// 与 git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
|
|
986
|
+
* 其余显式协议(ext::/file:// 等)一律拒绝 */
|
|
987
|
+
function assertGitUrlSafe(url) {
|
|
988
|
+
if (url.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git url must not start with "--": ${JSON.stringify(url)}`);
|
|
989
|
+
if (url.startsWith("https://") || url.startsWith("git@")) return;
|
|
990
|
+
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)}`);
|
|
991
|
+
}
|
|
992
|
+
/** git 源:clone --depth 1 + rev-parse HEAD 记录 commit;url/ref 前插 `--` 防选项注入 */
|
|
915
993
|
async function stageGit(source, ctx) {
|
|
994
|
+
assertGitUrlSafe(source.url);
|
|
995
|
+
if (source.ref?.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git ref must not start with "--": ${JSON.stringify(source.ref)}`);
|
|
916
996
|
const skillDir = `${ctx.stagingRoot}/repo`;
|
|
917
997
|
const args = [
|
|
918
998
|
"clone",
|
|
919
999
|
"--depth",
|
|
920
1000
|
"1",
|
|
921
1001
|
...source.ref ? ["--branch", source.ref] : [],
|
|
1002
|
+
"--",
|
|
922
1003
|
source.url,
|
|
923
1004
|
skillDir
|
|
924
1005
|
];
|
|
@@ -949,7 +1030,7 @@ function sha256Hex(data) {
|
|
|
949
1030
|
return createHash("sha256").update(data).digest("hex");
|
|
950
1031
|
}
|
|
951
1032
|
const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
952
|
-
/** http
|
|
1033
|
+
/** http 源:下载归档(Content-Length + 流式累计上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
|
|
953
1034
|
async function stageHttp(source, ctx, expectedSha256) {
|
|
954
1035
|
const fetchImpl = ctx.fetchImpl ?? fetch;
|
|
955
1036
|
let res;
|
|
@@ -959,18 +1040,18 @@ async function stageHttp(source, ctx, expectedSha256) {
|
|
|
959
1040
|
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
|
|
960
1041
|
}
|
|
961
1042
|
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
962
|
-
const data =
|
|
1043
|
+
const data = await readResponseWithLimit(res, ctx.archiveLimits);
|
|
963
1044
|
if (expectedSha256 !== void 0) {
|
|
964
1045
|
const actual = sha256Hex(data);
|
|
965
1046
|
if (actual !== expectedSha256) throw new WebSkillError("INSTALL_FAILED", `Archive checksum mismatch: expected ${expectedSha256}, got ${actual}`);
|
|
966
1047
|
}
|
|
967
1048
|
const contentDir = `${ctx.stagingRoot}/content`;
|
|
968
1049
|
await ctx.fs.mkdir(contentDir);
|
|
969
|
-
if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir);
|
|
1050
|
+
if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir, ctx.archiveLimits);
|
|
970
1051
|
else {
|
|
971
1052
|
const archivePath = `${ctx.stagingRoot}/archive.tar`;
|
|
972
1053
|
await ctx.fs.writeBinary(archivePath, data);
|
|
973
|
-
await extractTarFile(toPlatform(archivePath), toPlatform(contentDir));
|
|
1054
|
+
await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir), ctx.archiveLimits);
|
|
974
1055
|
}
|
|
975
1056
|
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
976
1057
|
if (await ctx.fs.exists(packFilePath)) return {
|
|
@@ -1002,16 +1083,27 @@ async function stageLocal(source, ctx) {
|
|
|
1002
1083
|
source
|
|
1003
1084
|
};
|
|
1004
1085
|
}
|
|
1005
|
-
/** npm
|
|
1086
|
+
/** npm 包名(@scope/name 或 name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
|
|
1087
|
+
const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
1088
|
+
const VERSION_RE = /^[a-z0-9._~^>=<x*+-]+$/i;
|
|
1089
|
+
function assertNpmSpecSafe(source) {
|
|
1090
|
+
const name = source.packageName;
|
|
1091
|
+
const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
|
|
1092
|
+
if (name.startsWith("--") || !PACKAGE_NAME_RE.test(name) && !isLocalPath) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package name: ${JSON.stringify(name)}`);
|
|
1093
|
+
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)}`);
|
|
1094
|
+
}
|
|
1095
|
+
/** npm 源:npm pack → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
|
|
1006
1096
|
async function stageNpm(source, ctx) {
|
|
1097
|
+
assertNpmSpecSafe(source);
|
|
1007
1098
|
const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
|
|
1008
1099
|
let stdout;
|
|
1009
1100
|
try {
|
|
1010
1101
|
({stdout} = await execFileP("npm", [
|
|
1011
1102
|
"pack",
|
|
1012
|
-
spec,
|
|
1013
1103
|
"--pack-destination",
|
|
1014
|
-
ctx.stagingRoot
|
|
1104
|
+
ctx.stagingRoot,
|
|
1105
|
+
"--",
|
|
1106
|
+
spec
|
|
1015
1107
|
]));
|
|
1016
1108
|
} catch (e) {
|
|
1017
1109
|
throw commandFailed("npm", e);
|
|
@@ -1021,7 +1113,7 @@ async function stageNpm(source, ctx) {
|
|
|
1021
1113
|
if (!tarball || !await ctx.fs.exists(tgzPath)) throw new WebSkillError("INSTALL_FAILED", `npm pack output not found in staging directory: ${tarball ?? "(empty output)"}`);
|
|
1022
1114
|
const pkgDir = `${ctx.stagingRoot}/pkg`;
|
|
1023
1115
|
await ctx.fs.mkdir(pkgDir);
|
|
1024
|
-
await extractTarFile(toPlatform(tgzPath), toPlatform(pkgDir));
|
|
1116
|
+
await extractTarFile(toPlatform$1(tgzPath), toPlatform$1(pkgDir));
|
|
1025
1117
|
const pkgRoot = `${pkgDir}/package`;
|
|
1026
1118
|
const skillDir = await ctx.fs.exists(`${pkgRoot}/skill/SKILL.md`) ? `${pkgRoot}/skill` : pkgRoot;
|
|
1027
1119
|
let packageName = source.packageName;
|
|
@@ -1112,15 +1204,13 @@ async function readManifest(fs, skillRoot) {
|
|
|
1112
1204
|
throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1113
1205
|
}
|
|
1114
1206
|
}
|
|
1115
|
-
/**
|
|
1207
|
+
/** walk 实际目录与 manifest 比对:hash 不一致 / 清单外新增 / 清单内缺失三类全空才 ok */
|
|
1116
1208
|
async function verifyIntegrity(fs, skillRoot) {
|
|
1117
1209
|
const manifest = await readManifest(fs, skillRoot);
|
|
1118
1210
|
const actualHashes = /* @__PURE__ */ new Map();
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
}
|
|
1123
|
-
return verifyManifest(manifest, actualHashes);
|
|
1211
|
+
const actualFiles = await walkFiles(fs, skillRoot);
|
|
1212
|
+
for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
|
|
1213
|
+
return verifyManifest(manifest, actualHashes, actualFiles);
|
|
1124
1214
|
}
|
|
1125
1215
|
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
1126
1216
|
const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
|
|
@@ -1134,12 +1224,16 @@ var SkillManager = class {
|
|
|
1134
1224
|
#fs;
|
|
1135
1225
|
#fetchImpl;
|
|
1136
1226
|
#schemaInference;
|
|
1227
|
+
#archiveLimits;
|
|
1228
|
+
#onChanged;
|
|
1137
1229
|
#schemaInferer = new OxcSchemaInferer();
|
|
1138
1230
|
constructor(deps) {
|
|
1139
1231
|
this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
|
|
1140
1232
|
this.#fs = deps.fs ?? new NodeFS();
|
|
1141
1233
|
this.#fetchImpl = deps.fetchImpl;
|
|
1142
1234
|
this.#schemaInference = deps.schemaInference ?? true;
|
|
1235
|
+
this.#archiveLimits = deps.archiveLimits;
|
|
1236
|
+
this.#onChanged = deps.onChanged;
|
|
1143
1237
|
}
|
|
1144
1238
|
async install(source, options) {
|
|
1145
1239
|
const fs = this.#fs;
|
|
@@ -1150,7 +1244,8 @@ var SkillManager = class {
|
|
|
1150
1244
|
const ctx = {
|
|
1151
1245
|
fs,
|
|
1152
1246
|
stagingRoot,
|
|
1153
|
-
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
1247
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {},
|
|
1248
|
+
...this.#archiveLimits ? { archiveLimits: this.#archiveLimits } : {}
|
|
1154
1249
|
};
|
|
1155
1250
|
let staged;
|
|
1156
1251
|
switch (source.type) {
|
|
@@ -1187,21 +1282,34 @@ var SkillManager = class {
|
|
|
1187
1282
|
const errors = report.issues.filter((i) => i.severity === "error");
|
|
1188
1283
|
throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
1189
1284
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
await copyDir(fs, finalDir, targetDir);
|
|
1193
|
-
await this.#inferSkillSchemas(targetDir);
|
|
1194
|
-
const manifest = await createManifest(fs, targetDir, {
|
|
1285
|
+
await this.#inferSkillSchemas(finalDir);
|
|
1286
|
+
const manifest = await createManifest(fs, finalDir, {
|
|
1195
1287
|
name,
|
|
1196
1288
|
...version ? { version } : {},
|
|
1197
1289
|
source: staged.source
|
|
1198
1290
|
});
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1291
|
+
targetDir = `${this.#managedRoot}/${name}`;
|
|
1292
|
+
const backupDir = `${stagingRoot}/backup`;
|
|
1293
|
+
const hadPrevious = await fs.exists(targetDir);
|
|
1294
|
+
if (hadPrevious) await copyDir(fs, targetDir, backupDir);
|
|
1295
|
+
let committed = false;
|
|
1296
|
+
try {
|
|
1297
|
+
if (hadPrevious) await fs.remove(targetDir, { recursive: true });
|
|
1298
|
+
await copyDir(fs, finalDir, targetDir);
|
|
1299
|
+
committed = true;
|
|
1300
|
+
await upsertLockEntry(fs, this.#managedRoot, name, {
|
|
1301
|
+
digest: manifest.integrity.digest,
|
|
1302
|
+
installedAt: manifest.installedAt,
|
|
1303
|
+
source: staged.source
|
|
1304
|
+
});
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
if (committed) await removeDirQuiet(fs, targetDir);
|
|
1307
|
+
if (hadPrevious) await copyDir(fs, backupDir, targetDir);
|
|
1308
|
+
targetDir = void 0;
|
|
1309
|
+
throw e;
|
|
1310
|
+
}
|
|
1204
1311
|
targetDir = void 0;
|
|
1312
|
+
this.#onChanged?.();
|
|
1205
1313
|
return manifest;
|
|
1206
1314
|
} catch (e) {
|
|
1207
1315
|
if (targetDir) await removeDirQuiet(fs, targetDir);
|
|
@@ -1243,11 +1351,7 @@ var SkillManager = class {
|
|
|
1243
1351
|
}
|
|
1244
1352
|
const manifests = [];
|
|
1245
1353
|
for (const entry of pack.manifest.skills) {
|
|
1246
|
-
const
|
|
1247
|
-
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
1248
|
-
await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
|
|
1249
|
-
installed.push(targetDir);
|
|
1250
|
-
const manifest = await createManifest(fs, targetDir, {
|
|
1354
|
+
const manifest = await createManifest(fs, `${finalRoot}/${entry.name}`, {
|
|
1251
1355
|
name: entry.name,
|
|
1252
1356
|
...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
|
|
1253
1357
|
source
|
|
@@ -1255,11 +1359,36 @@ var SkillManager = class {
|
|
|
1255
1359
|
if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
|
|
1256
1360
|
manifests.push(manifest);
|
|
1257
1361
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1362
|
+
const swapped = [];
|
|
1363
|
+
try {
|
|
1364
|
+
for (const manifest of manifests) {
|
|
1365
|
+
const targetDir = `${this.#managedRoot}/${manifest.name}`;
|
|
1366
|
+
const backupDir = `${stagingRoot}/backup/${manifest.name}`;
|
|
1367
|
+
if (await fs.exists(targetDir)) {
|
|
1368
|
+
await copyDir(fs, targetDir, backupDir);
|
|
1369
|
+
await fs.remove(targetDir, { recursive: true });
|
|
1370
|
+
}
|
|
1371
|
+
await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
|
|
1372
|
+
installed.push(targetDir);
|
|
1373
|
+
swapped.push({
|
|
1374
|
+
targetDir,
|
|
1375
|
+
backupDir
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
for (const manifest of manifests) await upsertLockEntry(fs, this.#managedRoot, manifest.name, {
|
|
1379
|
+
digest: manifest.integrity.digest,
|
|
1380
|
+
installedAt: manifest.installedAt,
|
|
1381
|
+
source
|
|
1382
|
+
});
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
for (const { targetDir, backupDir } of swapped) {
|
|
1385
|
+
await removeDirQuiet(fs, targetDir);
|
|
1386
|
+
if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
|
|
1387
|
+
}
|
|
1388
|
+
installed.length = 0;
|
|
1389
|
+
throw e;
|
|
1390
|
+
}
|
|
1391
|
+
this.#onChanged?.();
|
|
1263
1392
|
return manifests;
|
|
1264
1393
|
} catch (e) {
|
|
1265
1394
|
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
@@ -1295,6 +1424,7 @@ var SkillManager = class {
|
|
|
1295
1424
|
try {
|
|
1296
1425
|
await this.#fs.remove(targetDir, { recursive: true });
|
|
1297
1426
|
await removeLockEntry(this.#fs, this.#managedRoot, name);
|
|
1427
|
+
this.#onChanged?.();
|
|
1298
1428
|
} catch (e) {
|
|
1299
1429
|
throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf(e)}`, e);
|
|
1300
1430
|
}
|