@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.
@@ -1,21 +1,21 @@
1
- import { D as verifyManifest, E as validateSkills, S as parseSkillPackManifest, T as resolveInsideRoot, _ as exportSkills, f as buildManifest, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, v as isValidSkillName, x as parseSkillMarkdown } from "./dist-DvoBsChX.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-DAn07zHu.js";
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$4 = (p) => p.split("/").join(path.sep);
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$4(this.#root));
51
- const targetReal = await realpathNearest(toPlatform$4(p));
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$4(p), "utf8");
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$4(p);
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$4(p));
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$4(p);
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$4(p));
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$4(p));
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$4(p), { withFileTypes: true });
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$4(p), d.name)),
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$4(p), { recursive: true });
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$4(p), { recursive: options?.recursive ?? false });
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
- return await import(`${pathToFileURL(toPlatform$3(scriptPath)).href}?t=${Date.now()}`);
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)。每个条目路径过 resolveInsideRoot:
837
+ * zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
822
838
  * 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
823
839
  */
824
- async function extractZipData(fs, data, destRoot) {
825
- let entries;
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 源:clone --depth 1 + rev-parse HEAD 记录 commit */
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 源:下载归档(可选 expectedSha256 校验包体)→ 解包 → 定位技能根 */
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 = new Uint8Array(await res.arrayBuffer());
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 源:npm pack 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根 */
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
- /** manifest.files 逐文件重算 sha256 比对(core 纯逻辑校验) */
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
- for (const file of manifest.files) {
1174
- const path = `${skillRoot}/${file.path}`;
1175
- if (await fs.exists(path)) actualHashes.set(file.path, sha256Hex(await fs.readBinary(path)));
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(调用方负责计算 actualHashes) */
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 || actual !== file.sha256) mismatches.push(file.path);
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 { renderAvailableSkillsXml as C, verifyManifest as D, validateSkills as E, xmlRenderer as O, parseSkillPackManifest as S, resolveInsideRoot as T, exportSkills as _, SKILL_NAME_PATTERN as a, normalizePath as b, SkillReader as c, buildCatalog as d, buildManifest as f, escapeXml as g, computeDigest as h, SKILL_NAME_MAX_LENGTH as i, WebSkillError as l, checkSkillRules as m, SKILLS_LOCKFILE as n, SKILL_PACK_FILE as o, checkDependencyCycles as p, SKILL_MANIFEST_FILE as r, SkillDiscovery as s, MemoryFS as t, assertSafePathSegment as u, isValidSkillName as v, renderCatalogJson as w, parseSkillMarkdown as x, jsonRenderer as y };
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 };
@@ -1,6 +1,6 @@
1
- import { L as SkillManifest, N as SkillDocument, S as FileSystemProvider, c as LlmClient, j as SkillCatalogEntry, u as LlmMessage, v as UiBridge } from "./types-CgNC-oQu-DYrxhD3z.js";
2
- import { ct as WebSkillRuntime, q as RuntimeRun } from "./index-BSS3EWY-.js";
3
- import { f as SkillManager } from "./index-npFMt2Zw.js";
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 技能被路由过滤 */
@@ -1,8 +1,8 @@
1
- import { E as validateSkills, T as resolveInsideRoot, l as WebSkillError, u as assertSafePathSegment, v as isValidSkillName } from "./dist-DvoBsChX.js";
2
- import { i as NodeFS } from "./dist-BVXZF7H_.js";
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 JsonSchema, H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, S as FileSystemProvider, W as VerifyResult, _ as RenderResultRequest, o as InteractionRequest, s as InteractionResponse, v as UiBridge, x as FileStat } from "./types-CgNC-oQu-DYrxhD3z.js";
2
- import { $ as ToolDefinition, N as NetworkPolicy, X as ScriptExecutionContext, Y as SchemaInferer, Z as ScriptExecutor, b as FsArtifactStore, d as BridgeCapabilities, tt as ToolResult, u as ApprovalScope, x as FsMemoryStore } from "./index-BSS3EWY-.js";
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;