@webskill/sdk 0.1.4 → 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/dist/browser.d.ts +8 -4
- package/dist/browser.js +18 -20
- package/dist/{dist-MTc2KB03.js → dist-CfBOjJ4p.js} +1 -1
- package/dist/{dist-DAn07zHu.js → dist-DuGN5x0v.js} +100 -15
- package/dist/{dist-BVXZF7H_.js → dist-LIFS3ZjI.js} +95 -51
- package/dist/{dist-DvoBsChX.js → dist-fZFZaf43.js} +133 -7
- package/dist/governance.d.ts +9 -3
- package/dist/governance.js +18 -3
- package/dist/{index-npFMt2Zw.d.ts → index-Bun1aEf4.d.ts} +6 -2
- package/dist/{index-BSS3EWY-.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.js +20 -3
- package/dist/{testing-S8SN9Lc6.js → testing-BmR48Pn1.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CgNC-oQu-DYrxhD3z.d.ts → types-CgNC-oQu-DEcIBJKG.d.ts} +29 -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,21 +1,21 @@
|
|
|
1
|
-
import { D as
|
|
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";
|
|
16
16
|
|
|
17
17
|
//#region ../node/dist/index.js
|
|
18
|
-
const toPlatform$
|
|
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 最近现存祖先并拼回剩余段(写入目标尚不存在时的等价判定) */
|
|
@@ -47,41 +47,41 @@ var NodeFS = class {
|
|
|
47
47
|
/** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
|
|
48
48
|
async #assertContained(p) {
|
|
49
49
|
if (!this.#root) return;
|
|
50
|
-
const rootReal = await promises.realpath(toPlatform$
|
|
51
|
-
const targetReal = await realpathNearest(toPlatform$
|
|
50
|
+
const rootReal = await promises.realpath(toPlatform$3(this.#root));
|
|
51
|
+
const targetReal = await realpathNearest(toPlatform$3(p));
|
|
52
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
53
|
}
|
|
54
54
|
async readText(p) {
|
|
55
55
|
await this.#assertContained(p);
|
|
56
56
|
try {
|
|
57
|
-
return await promises.readFile(toPlatform$
|
|
57
|
+
return await promises.readFile(toPlatform$3(p), "utf8");
|
|
58
58
|
} catch (e) {
|
|
59
59
|
throw this.#mapError(e, p);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
async writeText(p, content) {
|
|
63
63
|
await this.#assertContained(p);
|
|
64
|
-
const target = toPlatform$
|
|
64
|
+
const target = toPlatform$3(p);
|
|
65
65
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
66
66
|
await promises.writeFile(target, content, "utf8");
|
|
67
67
|
}
|
|
68
68
|
async readBinary(p) {
|
|
69
69
|
await this.#assertContained(p);
|
|
70
70
|
try {
|
|
71
|
-
return await promises.readFile(toPlatform$
|
|
71
|
+
return await promises.readFile(toPlatform$3(p));
|
|
72
72
|
} catch (e) {
|
|
73
73
|
throw this.#mapError(e, p);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
async writeBinary(p, content) {
|
|
77
77
|
await this.#assertContained(p);
|
|
78
|
-
const target = toPlatform$
|
|
78
|
+
const target = toPlatform$3(p);
|
|
79
79
|
await promises.mkdir(path.dirname(target), { recursive: true });
|
|
80
80
|
await promises.writeFile(target, content);
|
|
81
81
|
}
|
|
82
82
|
async exists(p) {
|
|
83
83
|
try {
|
|
84
|
-
await promises.access(toPlatform$
|
|
84
|
+
await promises.access(toPlatform$3(p));
|
|
85
85
|
return true;
|
|
86
86
|
} catch {
|
|
87
87
|
return false;
|
|
@@ -89,7 +89,7 @@ var NodeFS = class {
|
|
|
89
89
|
}
|
|
90
90
|
async stat(p) {
|
|
91
91
|
try {
|
|
92
|
-
const s = await promises.stat(toPlatform$
|
|
92
|
+
const s = await promises.stat(toPlatform$3(p));
|
|
93
93
|
return {
|
|
94
94
|
path: toPosix(p),
|
|
95
95
|
type: s.isDirectory() ? "directory" : "file",
|
|
@@ -103,21 +103,21 @@ var NodeFS = class {
|
|
|
103
103
|
async list(p) {
|
|
104
104
|
let dirents;
|
|
105
105
|
try {
|
|
106
|
-
dirents = await promises.readdir(toPlatform$
|
|
106
|
+
dirents = await promises.readdir(toPlatform$3(p), { withFileTypes: true });
|
|
107
107
|
} catch (e) {
|
|
108
108
|
throw this.#mapError(e, p);
|
|
109
109
|
}
|
|
110
110
|
return dirents.map((d) => ({
|
|
111
|
-
path: toPosix(path.join(toPlatform$
|
|
111
|
+
path: toPosix(path.join(toPlatform$3(p), d.name)),
|
|
112
112
|
type: d.isDirectory() ? "directory" : "file"
|
|
113
113
|
}));
|
|
114
114
|
}
|
|
115
115
|
async mkdir(p) {
|
|
116
|
-
await promises.mkdir(toPlatform$
|
|
116
|
+
await promises.mkdir(toPlatform$3(p), { recursive: true });
|
|
117
117
|
}
|
|
118
118
|
async remove(p, options) {
|
|
119
119
|
try {
|
|
120
|
-
await promises.rm(toPlatform$
|
|
120
|
+
await promises.rm(toPlatform$3(p), { recursive: options?.recursive ?? false });
|
|
121
121
|
} catch (e) {
|
|
122
122
|
throw this.#mapError(e, p);
|
|
123
123
|
}
|
|
@@ -128,7 +128,6 @@ var NodeFS = class {
|
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
131
|
-
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
132
131
|
/**
|
|
133
132
|
* 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
|
|
134
133
|
* worker_threads 沙箱见 deferred-items D1。
|
|
@@ -148,7 +147,22 @@ var NodeScriptExecutor = class {
|
|
|
148
147
|
return hasTs ? tsPath : jsPath;
|
|
149
148
|
}
|
|
150
149
|
async #loadModule(scriptPath) {
|
|
151
|
-
|
|
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()}`)}`);
|
|
152
166
|
}
|
|
153
167
|
async loadDefinition(skillRoot, scriptName) {
|
|
154
168
|
const scriptPath = await this.#locateScript(skillRoot, scriptName);
|
|
@@ -319,7 +333,8 @@ var SandboxedScriptExecutor = class {
|
|
|
319
333
|
const result = await this.#runWorker({
|
|
320
334
|
mode: "load",
|
|
321
335
|
scriptPath: toPlatform$2(scriptPath),
|
|
322
|
-
scriptName
|
|
336
|
+
scriptName,
|
|
337
|
+
scriptSource: await this.#fs.readText(scriptPath)
|
|
323
338
|
});
|
|
324
339
|
if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
|
|
325
340
|
return {
|
|
@@ -352,7 +367,8 @@ var SandboxedScriptExecutor = class {
|
|
|
352
367
|
scriptName,
|
|
353
368
|
skillName: context.skillName,
|
|
354
369
|
runId: context.runId,
|
|
355
|
-
args
|
|
370
|
+
args,
|
|
371
|
+
scriptSource: await this.#fs.readText(scriptPath)
|
|
356
372
|
}, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
357
373
|
if (!result.ok) {
|
|
358
374
|
const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
|
|
@@ -818,33 +834,33 @@ const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
|
818
834
|
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
819
835
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
820
836
|
/**
|
|
821
|
-
* zip 解包(fflate
|
|
837
|
+
* zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
|
|
822
838
|
* 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
|
|
823
839
|
*/
|
|
824
|
-
async function extractZipData(fs, data, destRoot) {
|
|
825
|
-
|
|
826
|
-
try {
|
|
827
|
-
entries = unzipSync(data);
|
|
828
|
-
} catch (e) {
|
|
829
|
-
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${messageOf$4(e)}`, e);
|
|
830
|
-
}
|
|
831
|
-
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)) {
|
|
832
842
|
const target = resolveInsideRoot(destRoot, entryPath);
|
|
833
843
|
if (entryPath.endsWith("/")) await fs.mkdir(target);
|
|
834
844
|
else await fs.writeBinary(target, content);
|
|
835
845
|
}
|
|
836
846
|
}
|
|
837
847
|
/**
|
|
838
|
-
* tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot
|
|
839
|
-
*
|
|
848
|
+
* tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot 校验并累计
|
|
849
|
+
* 单条目/总体积上限(声明 size 预检),再解包;符号链接/硬链接条目不跟随(跳过)。
|
|
840
850
|
*/
|
|
841
|
-
async function extractTarFile(archivePath, destRoot) {
|
|
851
|
+
async function extractTarFile(archivePath, destRoot, limits) {
|
|
852
|
+
const { maxEntryBytes, maxTotalBytes } = resolveArchiveLimits(limits);
|
|
842
853
|
try {
|
|
843
854
|
const entryPaths = [];
|
|
855
|
+
let total = 0;
|
|
844
856
|
await tar.t({
|
|
845
857
|
file: toPlatform$1(archivePath),
|
|
846
858
|
onentry: (entry) => {
|
|
847
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`);
|
|
848
864
|
}
|
|
849
865
|
});
|
|
850
866
|
for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
|
|
@@ -951,28 +967,39 @@ const _execFileP = promisify(execFile);
|
|
|
951
967
|
const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
952
968
|
/**
|
|
953
969
|
* 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
|
|
954
|
-
* git 等原生 exe
|
|
970
|
+
* git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
|
|
955
971
|
*/
|
|
956
|
-
async function execFileP(command, args) {
|
|
972
|
+
async function execFileP(command, args, options = {}) {
|
|
973
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
957
974
|
if (process.platform === "win32" && command === "npm") return _execFileP("cmd", [
|
|
958
975
|
"/c",
|
|
959
976
|
command,
|
|
960
977
|
...args
|
|
961
|
-
]);
|
|
962
|
-
return _execFileP(command, args);
|
|
978
|
+
], { timeout: timeoutMs });
|
|
979
|
+
return _execFileP(command, args, { timeout: timeoutMs });
|
|
963
980
|
}
|
|
964
981
|
/** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
|
|
965
982
|
function commandFailed(command, e) {
|
|
966
983
|
return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf$2(e)}`, e);
|
|
967
984
|
}
|
|
968
|
-
/** 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 前插 `--` 防选项注入 */
|
|
969
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)}`);
|
|
970
996
|
const skillDir = `${ctx.stagingRoot}/repo`;
|
|
971
997
|
const args = [
|
|
972
998
|
"clone",
|
|
973
999
|
"--depth",
|
|
974
1000
|
"1",
|
|
975
1001
|
...source.ref ? ["--branch", source.ref] : [],
|
|
1002
|
+
"--",
|
|
976
1003
|
source.url,
|
|
977
1004
|
skillDir
|
|
978
1005
|
];
|
|
@@ -1003,7 +1030,7 @@ function sha256Hex(data) {
|
|
|
1003
1030
|
return createHash("sha256").update(data).digest("hex");
|
|
1004
1031
|
}
|
|
1005
1032
|
const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
|
|
1006
|
-
/** http
|
|
1033
|
+
/** http 源:下载归档(Content-Length + 流式累计上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
|
|
1007
1034
|
async function stageHttp(source, ctx, expectedSha256) {
|
|
1008
1035
|
const fetchImpl = ctx.fetchImpl ?? fetch;
|
|
1009
1036
|
let res;
|
|
@@ -1013,18 +1040,18 @@ async function stageHttp(source, ctx, expectedSha256) {
|
|
|
1013
1040
|
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
|
|
1014
1041
|
}
|
|
1015
1042
|
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
1016
|
-
const data =
|
|
1043
|
+
const data = await readResponseWithLimit(res, ctx.archiveLimits);
|
|
1017
1044
|
if (expectedSha256 !== void 0) {
|
|
1018
1045
|
const actual = sha256Hex(data);
|
|
1019
1046
|
if (actual !== expectedSha256) throw new WebSkillError("INSTALL_FAILED", `Archive checksum mismatch: expected ${expectedSha256}, got ${actual}`);
|
|
1020
1047
|
}
|
|
1021
1048
|
const contentDir = `${ctx.stagingRoot}/content`;
|
|
1022
1049
|
await ctx.fs.mkdir(contentDir);
|
|
1023
|
-
if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir);
|
|
1050
|
+
if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir, ctx.archiveLimits);
|
|
1024
1051
|
else {
|
|
1025
1052
|
const archivePath = `${ctx.stagingRoot}/archive.tar`;
|
|
1026
1053
|
await ctx.fs.writeBinary(archivePath, data);
|
|
1027
|
-
await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir));
|
|
1054
|
+
await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir), ctx.archiveLimits);
|
|
1028
1055
|
}
|
|
1029
1056
|
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
1030
1057
|
if (await ctx.fs.exists(packFilePath)) return {
|
|
@@ -1056,16 +1083,27 @@ async function stageLocal(source, ctx) {
|
|
|
1056
1083
|
source
|
|
1057
1084
|
};
|
|
1058
1085
|
}
|
|
1059
|
-
/** 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 前插 `--` 防选项注入 */
|
|
1060
1096
|
async function stageNpm(source, ctx) {
|
|
1097
|
+
assertNpmSpecSafe(source);
|
|
1061
1098
|
const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
|
|
1062
1099
|
let stdout;
|
|
1063
1100
|
try {
|
|
1064
1101
|
({stdout} = await execFileP("npm", [
|
|
1065
1102
|
"pack",
|
|
1066
|
-
spec,
|
|
1067
1103
|
"--pack-destination",
|
|
1068
|
-
ctx.stagingRoot
|
|
1104
|
+
ctx.stagingRoot,
|
|
1105
|
+
"--",
|
|
1106
|
+
spec
|
|
1069
1107
|
]));
|
|
1070
1108
|
} catch (e) {
|
|
1071
1109
|
throw commandFailed("npm", e);
|
|
@@ -1166,15 +1204,13 @@ async function readManifest(fs, skillRoot) {
|
|
|
1166
1204
|
throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
1167
1205
|
}
|
|
1168
1206
|
}
|
|
1169
|
-
/**
|
|
1207
|
+
/** walk 实际目录与 manifest 比对:hash 不一致 / 清单外新增 / 清单内缺失三类全空才 ok */
|
|
1170
1208
|
async function verifyIntegrity(fs, skillRoot) {
|
|
1171
1209
|
const manifest = await readManifest(fs, skillRoot);
|
|
1172
1210
|
const actualHashes = /* @__PURE__ */ new Map();
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1177
|
-
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);
|
|
1178
1214
|
}
|
|
1179
1215
|
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
1180
1216
|
const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
|
|
@@ -1188,12 +1224,16 @@ var SkillManager = class {
|
|
|
1188
1224
|
#fs;
|
|
1189
1225
|
#fetchImpl;
|
|
1190
1226
|
#schemaInference;
|
|
1227
|
+
#archiveLimits;
|
|
1228
|
+
#onChanged;
|
|
1191
1229
|
#schemaInferer = new OxcSchemaInferer();
|
|
1192
1230
|
constructor(deps) {
|
|
1193
1231
|
this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
|
|
1194
1232
|
this.#fs = deps.fs ?? new NodeFS();
|
|
1195
1233
|
this.#fetchImpl = deps.fetchImpl;
|
|
1196
1234
|
this.#schemaInference = deps.schemaInference ?? true;
|
|
1235
|
+
this.#archiveLimits = deps.archiveLimits;
|
|
1236
|
+
this.#onChanged = deps.onChanged;
|
|
1197
1237
|
}
|
|
1198
1238
|
async install(source, options) {
|
|
1199
1239
|
const fs = this.#fs;
|
|
@@ -1204,7 +1244,8 @@ var SkillManager = class {
|
|
|
1204
1244
|
const ctx = {
|
|
1205
1245
|
fs,
|
|
1206
1246
|
stagingRoot,
|
|
1207
|
-
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
|
|
1247
|
+
...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {},
|
|
1248
|
+
...this.#archiveLimits ? { archiveLimits: this.#archiveLimits } : {}
|
|
1208
1249
|
};
|
|
1209
1250
|
let staged;
|
|
1210
1251
|
switch (source.type) {
|
|
@@ -1268,6 +1309,7 @@ var SkillManager = class {
|
|
|
1268
1309
|
throw e;
|
|
1269
1310
|
}
|
|
1270
1311
|
targetDir = void 0;
|
|
1312
|
+
this.#onChanged?.();
|
|
1271
1313
|
return manifest;
|
|
1272
1314
|
} catch (e) {
|
|
1273
1315
|
if (targetDir) await removeDirQuiet(fs, targetDir);
|
|
@@ -1346,6 +1388,7 @@ var SkillManager = class {
|
|
|
1346
1388
|
installed.length = 0;
|
|
1347
1389
|
throw e;
|
|
1348
1390
|
}
|
|
1391
|
+
this.#onChanged?.();
|
|
1349
1392
|
return manifests;
|
|
1350
1393
|
} catch (e) {
|
|
1351
1394
|
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
@@ -1381,6 +1424,7 @@ var SkillManager = class {
|
|
|
1381
1424
|
try {
|
|
1382
1425
|
await this.#fs.remove(targetDir, { recursive: true });
|
|
1383
1426
|
await removeLockEntry(this.#fs, this.#managedRoot, name);
|
|
1427
|
+
this.#onChanged?.();
|
|
1384
1428
|
} catch (e) {
|
|
1385
1429
|
throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf(e)}`, e);
|
|
1386
1430
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parse } from "yaml";
|
|
2
|
-
import { zipSync } from "fflate";
|
|
2
|
+
import { Unzip, UnzipInflate, zipSync } from "fflate";
|
|
3
3
|
|
|
4
4
|
//#region ../core/dist/index.js
|
|
5
5
|
/**
|
|
@@ -39,16 +39,22 @@ async function buildManifest(input) {
|
|
|
39
39
|
files: input.files
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
/** 按 manifest.files 比对实际 hash
|
|
43
|
-
function verifyManifest(manifest, actualHashes) {
|
|
42
|
+
/** 按 manifest.files 比对实际 hash 与文件集(mismatches/extras/missing 三类全空才 ok) */
|
|
43
|
+
function verifyManifest(manifest, actualHashes, actualFiles = [...actualHashes.keys()]) {
|
|
44
44
|
const mismatches = [];
|
|
45
|
+
const missing = [];
|
|
46
|
+
const listed = new Set(manifest.files.map((f) => f.path));
|
|
45
47
|
for (const file of manifest.files) {
|
|
46
48
|
const actual = actualHashes.get(file.path);
|
|
47
|
-
if (actual === void 0
|
|
49
|
+
if (actual === void 0) missing.push(file.path);
|
|
50
|
+
else if (actual !== file.sha256) mismatches.push(file.path);
|
|
48
51
|
}
|
|
52
|
+
const extras = actualFiles.filter((p) => p !== "webskill.skill-manifest.json" && p !== "skills.lock.json" && !listed.has(p)).sort();
|
|
49
53
|
return {
|
|
50
|
-
ok: mismatches.length === 0,
|
|
51
|
-
mismatches
|
|
54
|
+
ok: mismatches.length === 0 && missing.length === 0 && extras.length === 0,
|
|
55
|
+
mismatches,
|
|
56
|
+
extras,
|
|
57
|
+
missing
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
const ROOT = "/";
|
|
@@ -407,6 +413,107 @@ function parseSkillPackManifest(text) {
|
|
|
407
413
|
}
|
|
408
414
|
return raw;
|
|
409
415
|
}
|
|
416
|
+
const DEFAULT_ARCHIVE_LIMITS = {
|
|
417
|
+
maxDownloadBytes: 64 * 1024 * 1024,
|
|
418
|
+
maxEntryBytes: 64 * 1024 * 1024,
|
|
419
|
+
maxTotalBytes: 256 * 1024 * 1024
|
|
420
|
+
};
|
|
421
|
+
function resolveArchiveLimits(limits) {
|
|
422
|
+
return {
|
|
423
|
+
...DEFAULT_ARCHIVE_LIMITS,
|
|
424
|
+
...limits
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function concat(chunks, size) {
|
|
428
|
+
const out = new Uint8Array(size);
|
|
429
|
+
let offset = 0;
|
|
430
|
+
for (const chunk of chunks) {
|
|
431
|
+
out.set(chunk, offset);
|
|
432
|
+
offset += chunk.length;
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
/** 流式解 zip(单条目/总双重上限);返回 [path, content][](目录条目以 / 结尾、内容为空) */
|
|
437
|
+
async function unzipWithLimits(data, limits) {
|
|
438
|
+
const { maxEntryBytes, maxTotalBytes } = resolveArchiveLimits(limits);
|
|
439
|
+
let eocd = false;
|
|
440
|
+
const scanFrom = Math.max(0, data.length - 65557);
|
|
441
|
+
for (let i = data.length - 22; i >= scanFrom; i--) if (data[i] === 80 && data[i + 1] === 75 && data[i + 2] === 5 && data[i + 3] === 6) {
|
|
442
|
+
eocd = true;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
if (!eocd) throw new WebSkillError("INSTALL_FAILED", "Failed to read zip archive: missing end-of-central-directory record");
|
|
446
|
+
const entries = [];
|
|
447
|
+
let total = 0;
|
|
448
|
+
let failure;
|
|
449
|
+
const unzip = new Unzip();
|
|
450
|
+
unzip.register(UnzipInflate);
|
|
451
|
+
unzip.onfile = (file) => {
|
|
452
|
+
if (file.name.endsWith("/")) {
|
|
453
|
+
entries.push([file.name, /* @__PURE__ */ new Uint8Array(0)]);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const chunks = [];
|
|
457
|
+
let size = 0;
|
|
458
|
+
file.ondata = (err, chunk, final) => {
|
|
459
|
+
if (err) {
|
|
460
|
+
failure = err;
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
size += chunk.length;
|
|
464
|
+
if (size > maxEntryBytes) {
|
|
465
|
+
failure = new WebSkillError("INSTALL_FAILED", `Archive entry exceeds the ${maxEntryBytes}-byte limit: ${file.name}`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
total += chunk.length;
|
|
469
|
+
if (total > maxTotalBytes) {
|
|
470
|
+
failure = new WebSkillError("INSTALL_FAILED", `Archive contents exceed the ${maxTotalBytes}-byte total limit`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
chunks.push(chunk);
|
|
474
|
+
if (final) entries.push([file.name, concat(chunks, size)]);
|
|
475
|
+
};
|
|
476
|
+
file.start();
|
|
477
|
+
};
|
|
478
|
+
try {
|
|
479
|
+
unzip.push(data, true);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
482
|
+
}
|
|
483
|
+
await new Promise((resolve) => {
|
|
484
|
+
setTimeout(resolve, 0);
|
|
485
|
+
});
|
|
486
|
+
if (failure) {
|
|
487
|
+
if (failure instanceof WebSkillError) throw failure;
|
|
488
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${failure instanceof Error ? failure.message : String(failure)}`, failure);
|
|
489
|
+
}
|
|
490
|
+
return entries;
|
|
491
|
+
}
|
|
492
|
+
/** 下载响应体:Content-Length 预检 + 流式累计上限 */
|
|
493
|
+
async function readResponseWithLimit(res, limits) {
|
|
494
|
+
const { maxDownloadBytes } = resolveArchiveLimits(limits);
|
|
495
|
+
const declared = res.headers.get("content-length");
|
|
496
|
+
if (declared !== null && Number(declared) > maxDownloadBytes) throw new WebSkillError("INSTALL_FAILED", `Download exceeds the ${maxDownloadBytes}-byte limit (Content-Length: ${declared})`);
|
|
497
|
+
if (!res.body) {
|
|
498
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
499
|
+
if (buf.length > maxDownloadBytes) throw new WebSkillError("INSTALL_FAILED", `Download exceeds the ${maxDownloadBytes}-byte limit`);
|
|
500
|
+
return buf;
|
|
501
|
+
}
|
|
502
|
+
const reader = res.body.getReader();
|
|
503
|
+
const chunks = [];
|
|
504
|
+
let size = 0;
|
|
505
|
+
for (;;) {
|
|
506
|
+
const { value, done } = await reader.read();
|
|
507
|
+
if (done) break;
|
|
508
|
+
size += value.length;
|
|
509
|
+
if (size > maxDownloadBytes) {
|
|
510
|
+
await reader.cancel().catch(() => void 0);
|
|
511
|
+
throw new WebSkillError("INSTALL_FAILED", `Download exceeds the ${maxDownloadBytes}-byte limit`);
|
|
512
|
+
}
|
|
513
|
+
chunks.push(value);
|
|
514
|
+
}
|
|
515
|
+
return concat(chunks, size);
|
|
516
|
+
}
|
|
410
517
|
/** 由条目列表构建 Catalog,保证按名称排序 */
|
|
411
518
|
function buildCatalog(entries) {
|
|
412
519
|
return { entries: [...entries].sort((a, b) => a.name.localeCompare(b.name)) };
|
|
@@ -523,6 +630,25 @@ var SkillDiscovery = class {
|
|
|
523
630
|
});
|
|
524
631
|
}
|
|
525
632
|
issues.push(...cycles.issues);
|
|
633
|
+
const admitted = new Set(entries.map((e) => e.name));
|
|
634
|
+
let changed = true;
|
|
635
|
+
while (changed) {
|
|
636
|
+
changed = false;
|
|
637
|
+
for (const entry of [...entries]) {
|
|
638
|
+
const missing = (adjacency.get(entry.name) ?? []).filter((d) => !admitted.has(d));
|
|
639
|
+
if (missing.length === 0) continue;
|
|
640
|
+
entries.splice(entries.indexOf(entry), 1);
|
|
641
|
+
admitted.delete(entry.name);
|
|
642
|
+
this.#index.delete(entry.name);
|
|
643
|
+
issues.push({
|
|
644
|
+
code: "SKILL_UNKNOWN_DEPENDENCY",
|
|
645
|
+
severity: "error",
|
|
646
|
+
message: `Skill "${entry.name}" was excluded: dependency ${JSON.stringify(missing[0])} is not in the final catalog`,
|
|
647
|
+
path: entry.root
|
|
648
|
+
});
|
|
649
|
+
changed = true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
526
652
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
527
653
|
return {
|
|
528
654
|
entries,
|
|
@@ -599,4 +725,4 @@ const xmlRenderer = {
|
|
|
599
725
|
};
|
|
600
726
|
|
|
601
727
|
//#endregion
|
|
602
|
-
export {
|
|
728
|
+
export { validateSkills as A, parseSkillPackManifest as C, resolveArchiveLimits as D, renderCatalogJson as E, xmlRenderer as M, resolveInsideRoot as O, parseSkillMarkdown as S, renderAvailableSkillsXml as T, escapeXml as _, SKILL_NAME_MAX_LENGTH as a, jsonRenderer as b, SkillDiscovery as c, assertSafePathSegment as d, buildCatalog as f, computeDigest as g, checkSkillRules as h, SKILL_MANIFEST_FILE as i, verifyManifest as j, unzipWithLimits as k, SkillReader as l, checkDependencyCycles as m, MemoryFS as n, SKILL_NAME_PATTERN as o, buildManifest as p, SKILLS_LOCKFILE as r, SKILL_PACK_FILE as s, DEFAULT_ARCHIVE_LIMITS as t, WebSkillError as u, exportSkills as v, readResponseWithLimit as w, normalizePath as x, isValidSkillName as y };
|
package/dist/governance.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { f as SkillManager } from "./index-
|
|
1
|
+
import { F as SkillDocument, N as SkillCatalogEntry, c as LlmClient, u as LlmMessage, v as UiBridge, w as FileSystemProvider, z as SkillManifest } from "./types-CgNC-oQu-DEcIBJKG.js";
|
|
2
|
+
import { $ as SkillStateGuard, lt as WebSkillRuntime, q as RuntimeRun } from "./index-COuOu6gw.js";
|
|
3
|
+
import { f as SkillManager } from "./index-Bun1aEf4.js";
|
|
4
4
|
//#region ../governance/dist/index.d.ts
|
|
5
5
|
//#region src/types.d.ts
|
|
6
6
|
type CandidateStatus = 'draft' | 'pending-review' | 'approved' | 'published' | 'rejected';
|
|
@@ -270,6 +270,12 @@ declare class SkillStatePolicy {
|
|
|
270
270
|
}): Promise<SkillState>;
|
|
271
271
|
canRoute(state: SkillState): boolean;
|
|
272
272
|
canExecute(state: SkillState): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* runtime SkillStateGuard 适配(0.1.5 一致拦截):
|
|
275
|
+
* canRead:active/deprecated 放行;canActivate/canExecute 与 canRoute/canExecute 语义对齐;
|
|
276
|
+
* quarantined/disabled 三入口全拦截。
|
|
277
|
+
*/
|
|
278
|
+
toSkillStateGuard(): SkillStateGuard;
|
|
273
279
|
/** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
|
|
274
280
|
assertExecutable(skillName: string): Promise<void>;
|
|
275
281
|
/** runtime catalogFilter 实现:quarantined/deprecated/disabled 技能被路由过滤 */
|
package/dist/governance.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as NodeFS } from "./dist-
|
|
1
|
+
import { A as validateSkills, O as resolveInsideRoot, d as assertSafePathSegment, u as WebSkillError, y as isValidSkillName } from "./dist-fZFZaf43.js";
|
|
2
|
+
import { i as NodeFS } from "./dist-LIFS3ZjI.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { mkdtemp } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
5
|
+
import { mkdtemp } from "node:fs/promises";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
|
|
8
8
|
//#region ../governance/dist/index.js
|
|
@@ -590,6 +590,21 @@ var SkillStatePolicy = class {
|
|
|
590
590
|
canExecute(state) {
|
|
591
591
|
return state === "active" || state === "deprecated";
|
|
592
592
|
}
|
|
593
|
+
/**
|
|
594
|
+
* runtime SkillStateGuard 适配(0.1.5 一致拦截):
|
|
595
|
+
* canRead:active/deprecated 放行;canActivate/canExecute 与 canRoute/canExecute 语义对齐;
|
|
596
|
+
* quarantined/disabled 三入口全拦截。
|
|
597
|
+
*/
|
|
598
|
+
toSkillStateGuard() {
|
|
599
|
+
return {
|
|
600
|
+
canRead: async (skillName) => {
|
|
601
|
+
const state = await this.getState(skillName);
|
|
602
|
+
return state === "active" || state === "deprecated";
|
|
603
|
+
},
|
|
604
|
+
canActivate: async (skillName) => this.canRoute(await this.getState(skillName)),
|
|
605
|
+
canExecute: async (skillName) => this.canExecute(await this.getState(skillName))
|
|
606
|
+
};
|
|
607
|
+
}
|
|
593
608
|
/** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
|
|
594
609
|
async assertExecutable(skillName) {
|
|
595
610
|
const state = await this.getState(skillName);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import {
|
|
1
|
+
import { C as FileStat, I as SkillInstallSource, K as VerifyResult, T as JsonSchema, W as SkillsLockfile, _ as RenderResultRequest, o as InteractionRequest, s as InteractionResponse, v as UiBridge, w as FileSystemProvider, y as ArchiveLimits, z as SkillManifest } from "./types-CgNC-oQu-DEcIBJKG.js";
|
|
2
|
+
import { N as NetworkPolicy, X as ScriptExecutionContext, Y as SchemaInferer, Z as ScriptExecutor, b as FsArtifactStore, d as BridgeCapabilities, et as ToolDefinition, nt as ToolResult, u as ApprovalScope, x as FsMemoryStore } from "./index-COuOu6gw.js";
|
|
3
3
|
import { Readable, Writable } from "node:stream";
|
|
4
4
|
//#region ../node/dist/index.d.ts
|
|
5
5
|
//#region src/fs/nodeFs.d.ts
|
|
@@ -158,6 +158,10 @@ declare class SkillManager {
|
|
|
158
158
|
fetchImpl?: typeof fetch;
|
|
159
159
|
/** D2 安装期 schema 预推导(默认 true,可关) */
|
|
160
160
|
schemaInference?: boolean;
|
|
161
|
+
/** 归档体积三重上限(缺省 DEFAULT_ARCHIVE_LIMITS) */
|
|
162
|
+
archiveLimits?: ArchiveLimits;
|
|
163
|
+
/** install/uninstall 成功后的变更回调(宿主接线缓存失效,如 WebSkillRuntime.invalidate) */
|
|
164
|
+
onChanged?: () => void;
|
|
161
165
|
});
|
|
162
166
|
install(source: SkillInstallSource, options?: {
|
|
163
167
|
expectedSha256?: string;
|