@webskill/sdk 0.1.4 → 0.2.0

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,20 @@
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 unzipWithLimits, C as parseSkillMarkdown, M as verifyManifest, O as resolveArchiveLimits, T as readResponseWithLimit, b as isValidSkillName, f as atomicWriteText, i as SKILL_MANIFEST_FILE, j as validateSkills, k as resolveInsideRoot, m as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, w as parseSkillPackManifest, y as exportSkills } from "./dist-D0saNPi_.js";
2
+ import { A as isNetworkAllowed, C as bridgeError, M as networkUrlHost, N as normalizeToolContent, P as parseBridgeRequest, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-BS5OpedX.js";
3
+ import { createRequire } from "node:module";
3
4
  import { unzipSync, zipSync } from "fflate";
4
- import { existsSync, promises, readFileSync } from "node:fs";
5
+ import { existsSync, promises } from "node:fs";
5
6
  import path from "node:path";
7
+ import { tmpdir } from "node:os";
6
8
  import { fileURLToPath, pathToFileURL } from "node:url";
7
9
  import { format, promisify } from "node:util";
8
10
  import { Worker } from "node:worker_threads";
9
- 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
- import * as tar from "tar";
14
13
  import { execFile } from "node:child_process";
15
14
  import { createHash } from "node:crypto";
16
15
 
17
16
  //#region ../node/dist/index.js
18
- const toPlatform$4 = (p) => p.split("/").join(path.sep);
17
+ const toPlatform$3 = (p) => p.split("/").join(path.sep);
19
18
  const toPosix = (p) => p.split(path.sep).join("/");
20
19
  const isEnoent = (e) => typeof e === "object" && e !== null && e.code === "ENOENT";
21
20
  /** realpath 最近现存祖先并拼回剩余段(写入目标尚不存在时的等价判定) */
@@ -47,41 +46,41 @@ var NodeFS = class {
47
46
  /** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
48
47
  async #assertContained(p) {
49
48
  if (!this.#root) return;
50
- const rootReal = await promises.realpath(toPlatform$4(this.#root));
51
- const targetReal = await realpathNearest(toPlatform$4(p));
49
+ const rootReal = await promises.realpath(toPlatform$3(this.#root));
50
+ const targetReal = await realpathNearest(toPlatform$3(p));
52
51
  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
52
  }
54
53
  async readText(p) {
55
54
  await this.#assertContained(p);
56
55
  try {
57
- return await promises.readFile(toPlatform$4(p), "utf8");
56
+ return await promises.readFile(toPlatform$3(p), "utf8");
58
57
  } catch (e) {
59
58
  throw this.#mapError(e, p);
60
59
  }
61
60
  }
62
61
  async writeText(p, content) {
63
62
  await this.#assertContained(p);
64
- const target = toPlatform$4(p);
63
+ const target = toPlatform$3(p);
65
64
  await promises.mkdir(path.dirname(target), { recursive: true });
66
65
  await promises.writeFile(target, content, "utf8");
67
66
  }
68
67
  async readBinary(p) {
69
68
  await this.#assertContained(p);
70
69
  try {
71
- return await promises.readFile(toPlatform$4(p));
70
+ return await promises.readFile(toPlatform$3(p));
72
71
  } catch (e) {
73
72
  throw this.#mapError(e, p);
74
73
  }
75
74
  }
76
75
  async writeBinary(p, content) {
77
76
  await this.#assertContained(p);
78
- const target = toPlatform$4(p);
77
+ const target = toPlatform$3(p);
79
78
  await promises.mkdir(path.dirname(target), { recursive: true });
80
79
  await promises.writeFile(target, content);
81
80
  }
82
81
  async exists(p) {
83
82
  try {
84
- await promises.access(toPlatform$4(p));
83
+ await promises.access(toPlatform$3(p));
85
84
  return true;
86
85
  } catch {
87
86
  return false;
@@ -89,7 +88,7 @@ var NodeFS = class {
89
88
  }
90
89
  async stat(p) {
91
90
  try {
92
- const s = await promises.stat(toPlatform$4(p));
91
+ const s = await promises.stat(toPlatform$3(p));
93
92
  return {
94
93
  path: toPosix(p),
95
94
  type: s.isDirectory() ? "directory" : "file",
@@ -103,32 +102,39 @@ var NodeFS = class {
103
102
  async list(p) {
104
103
  let dirents;
105
104
  try {
106
- dirents = await promises.readdir(toPlatform$4(p), { withFileTypes: true });
105
+ dirents = await promises.readdir(toPlatform$3(p), { withFileTypes: true });
107
106
  } catch (e) {
108
107
  throw this.#mapError(e, p);
109
108
  }
110
109
  return dirents.map((d) => ({
111
- path: toPosix(path.join(toPlatform$4(p), d.name)),
110
+ path: toPosix(path.join(toPlatform$3(p), d.name)),
112
111
  type: d.isDirectory() ? "directory" : "file"
113
112
  }));
114
113
  }
115
114
  async mkdir(p) {
116
- await promises.mkdir(toPlatform$4(p), { recursive: true });
115
+ await promises.mkdir(toPlatform$3(p), { recursive: true });
117
116
  }
118
117
  async remove(p, options) {
119
118
  try {
120
- await promises.rm(toPlatform$4(p), { recursive: options?.recursive ?? false });
119
+ await promises.rm(toPlatform$3(p), { recursive: options?.recursive ?? false });
121
120
  } catch (e) {
122
121
  throw this.#mapError(e, p);
123
122
  }
124
123
  }
124
+ async rename(from, to) {
125
+ await promises.mkdir(path.dirname(toPlatform$3(to)), { recursive: true });
126
+ try {
127
+ await promises.rename(toPlatform$3(from), toPlatform$3(to));
128
+ } catch (e) {
129
+ throw this.#mapError(e, from);
130
+ }
131
+ }
125
132
  #mapError(e, p) {
126
133
  if (isEnoent(e)) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${p}`);
127
134
  return e;
128
135
  }
129
136
  };
130
137
  const baseName$1 = (p) => p.split("/").pop() ?? p;
131
- const toPlatform$3 = (p) => p.split("/").join(path.sep);
132
138
  /**
133
139
  * 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
134
140
  * worker_threads 沙箱见 deferred-items D1。
@@ -148,7 +154,22 @@ var NodeScriptExecutor = class {
148
154
  return hasTs ? tsPath : jsPath;
149
155
  }
150
156
  async #loadModule(scriptPath) {
151
- return await import(`${pathToFileURL(toPlatform$3(scriptPath)).href}?t=${Date.now()}`);
157
+ if (scriptPath.endsWith(".ts")) {
158
+ const source = await this.#fs.readText(scriptPath);
159
+ const dir = await promises.mkdtemp(path.join(tmpdir(), "webskill-ts-"));
160
+ try {
161
+ const file = path.join(dir, "script.ts");
162
+ await promises.writeFile(file, source);
163
+ return await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
164
+ } finally {
165
+ await promises.rm(dir, {
166
+ recursive: true,
167
+ force: true
168
+ });
169
+ }
170
+ }
171
+ const source = await this.#fs.readText(scriptPath);
172
+ return await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(`${source}\n//# ${Date.now()}`)}`);
152
173
  }
153
174
  async loadDefinition(skillRoot, scriptName) {
154
175
  const scriptPath = await this.#locateScript(skillRoot, scriptName);
@@ -319,7 +340,8 @@ var SandboxedScriptExecutor = class {
319
340
  const result = await this.#runWorker({
320
341
  mode: "load",
321
342
  scriptPath: toPlatform$2(scriptPath),
322
- scriptName
343
+ scriptName,
344
+ scriptSource: await this.#fs.readText(scriptPath)
323
345
  });
324
346
  if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
325
347
  return {
@@ -352,7 +374,8 @@ var SandboxedScriptExecutor = class {
352
374
  scriptName,
353
375
  skillName: context.skillName,
354
376
  runId: context.runId,
355
- args
377
+ args,
378
+ scriptSource: await this.#fs.readText(scriptPath)
356
379
  }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
357
380
  if (!result.ok) {
358
381
  const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
@@ -496,6 +519,16 @@ var SandboxedScriptExecutor = class {
496
519
  }
497
520
  }
498
521
  };
522
+ const require_ = createRequire(import.meta.url);
523
+ let parseSyncCache;
524
+ function loadParseSync() {
525
+ if (!parseSyncCache) try {
526
+ parseSyncCache = require_("oxc-parser").parseSync;
527
+ } catch (e) {
528
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"oxc-parser\" package is required for schema inference; install it first (npm i oxc-parser)", e);
529
+ }
530
+ return parseSyncCache;
531
+ }
499
532
  const PLACEHOLDER_PREFIX = "Inferred placeholder (unsupported type: ";
500
533
  const placeholder = (text) => ({
501
534
  type: "string",
@@ -545,7 +578,7 @@ var TypeMapper = class {
545
578
  };
546
579
  return placeholder(this.#text(t));
547
580
  }
548
- #union(t, seen) {
581
+ #union(t, _seen) {
549
582
  const values = [];
550
583
  for (const member of t.types ?? []) {
551
584
  const value = member.literal?.value ?? member.value;
@@ -645,7 +678,7 @@ var OxcSchemaInferer = class {
645
678
  let program;
646
679
  let comments;
647
680
  try {
648
- const result = parseSync(options?.fileName ?? "script.ts", source);
681
+ const result = loadParseSync()(options?.fileName ?? "script.ts", source);
649
682
  if (result.errors?.length > 0) return void 0;
650
683
  program = result.program;
651
684
  comments = result.comments ?? [];
@@ -661,7 +694,7 @@ var OxcSchemaInferer = class {
661
694
  const jsdoc = comments.filter((c) => c.type === "Block" && typeof c.value === "string" && c.value.includes("@param") && typeof c.end === "number" && c.end <= runStart).sort((a, b) => b.end - a.end)[0];
662
695
  if (!jsdoc) return void 0;
663
696
  const reparse = (typeText) => {
664
- const r = parseSync("__t.ts", `type __T = ${typeText};`);
697
+ const r = loadParseSync()("__t.ts", `type __T = ${typeText};`);
665
698
  if (r.errors?.length > 0) return void 0;
666
699
  return r.program.body.find((n) => n.type === "TSTypeAliasDeclaration")?.typeAnnotation;
667
700
  };
@@ -817,38 +850,46 @@ var CliUiBridge = class {
817
850
  const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
818
851
  const toPlatform$1 = (p) => p.split("/").join(path.sep);
819
852
  const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
853
+ let tarModule$1;
854
+ async function loadTar$1() {
855
+ tarModule$1 ??= await import("tar").catch((e) => {
856
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
857
+ });
858
+ return tarModule$1;
859
+ }
820
860
  /**
821
- * zip 解包(fflate)。每个条目路径过 resolveInsideRoot:
861
+ * zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
822
862
  * 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
823
863
  */
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)) {
864
+ async function extractZipData(fs, data, destRoot, limits) {
865
+ for (const [entryPath, content] of await unzipWithLimits(data, limits)) {
832
866
  const target = resolveInsideRoot(destRoot, entryPath);
833
867
  if (entryPath.endsWith("/")) await fs.mkdir(target);
834
868
  else await fs.writeBinary(target, content);
835
869
  }
836
870
  }
837
871
  /**
838
- * tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot 校验,
839
- * 再解包;符号链接/硬链接条目不跟随(跳过)。
872
+ * tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot 校验并累计
873
+ * 单条目/总体积上限(声明 size 预检),再解包;符号链接/硬链接条目不跟随(跳过)。
840
874
  */
841
- async function extractTarFile(archivePath, destRoot) {
875
+ async function extractTarFile(archivePath, destRoot, limits) {
876
+ const { maxEntryBytes, maxTotalBytes } = resolveArchiveLimits(limits);
842
877
  try {
843
878
  const entryPaths = [];
844
- await tar.t({
879
+ let total = 0;
880
+ const tarMod = await loadTar$1();
881
+ await tarMod.t({
845
882
  file: toPlatform$1(archivePath),
846
883
  onentry: (entry) => {
847
884
  entryPaths.push(entry.path);
885
+ const size = typeof entry.size === "number" ? entry.size : 0;
886
+ if (size > maxEntryBytes) throw new WebSkillError("INSTALL_FAILED", `Archive entry exceeds the ${maxEntryBytes}-byte limit: ${entry.path}`);
887
+ total += size;
888
+ if (total > maxTotalBytes) throw new WebSkillError("INSTALL_FAILED", `Archive contents exceed the ${maxTotalBytes}-byte total limit`);
848
889
  }
849
890
  });
850
891
  for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
851
- await tar.x({
892
+ await tarMod.x({
852
893
  file: toPlatform$1(archivePath),
853
894
  cwd: toPlatform$1(destRoot),
854
895
  filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
@@ -900,6 +941,13 @@ async function removeDirQuiet(fs, dir) {
900
941
  } catch {}
901
942
  }
902
943
  const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
944
+ let tarModule;
945
+ async function loadTar() {
946
+ tarModule ??= await import("tar").catch((e) => {
947
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
948
+ });
949
+ return tarModule;
950
+ }
903
951
  /** 导出技能目录全部文件(含 manifest)为 zip/tar 归档,返回 outPath */
904
952
  async function exportArchive(fs, skillRoot, options) {
905
953
  try {
@@ -907,7 +955,7 @@ async function exportArchive(fs, skillRoot, options) {
907
955
  const files = {};
908
956
  for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
909
957
  await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
910
- } else await tar.c({
958
+ } else await (await loadTar()).c({
911
959
  file: toPlatform$1(options.outPath),
912
960
  cwd: toPlatform$1(skillRoot)
913
961
  }, ["."]);
@@ -929,7 +977,7 @@ async function readArchiveManifest(fs, archivePath) {
929
977
  }
930
978
  let manifestText;
931
979
  try {
932
- await tar.t({
980
+ await (await loadTar()).t({
933
981
  file: toPlatform$1(archivePath),
934
982
  onentry: (entry) => {
935
983
  if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
@@ -951,28 +999,39 @@ const _execFileP = promisify(execFile);
951
999
  const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
952
1000
  /**
953
1001
  * 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
954
- * git 等原生 exe 不受影响。
1002
+ * git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
955
1003
  */
956
- async function execFileP(command, args) {
1004
+ async function execFileP(command, args, options = {}) {
1005
+ const timeoutMs = options.timeoutMs ?? 12e4;
957
1006
  if (process.platform === "win32" && command === "npm") return _execFileP("cmd", [
958
1007
  "/c",
959
1008
  command,
960
1009
  ...args
961
- ]);
962
- return _execFileP(command, args);
1010
+ ], { timeout: timeoutMs });
1011
+ return _execFileP(command, args, { timeout: timeoutMs });
963
1012
  }
964
1013
  /** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
965
1014
  function commandFailed(command, e) {
966
1015
  return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf$2(e)}`, e);
967
1016
  }
968
- /** git 源:clone --depth 1 + rev-parse HEAD 记录 commit */
1017
+ /** git url 协议白名单:https:// git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
1018
+ * 其余显式协议(ext::/file:// 等)一律拒绝 */
1019
+ function assertGitUrlSafe(url) {
1020
+ if (url.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git url must not start with "--": ${JSON.stringify(url)}`);
1021
+ if (url.startsWith("https://") || url.startsWith("git@")) return;
1022
+ 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)}`);
1023
+ }
1024
+ /** git 源:clone --depth 1 + rev-parse HEAD 记录 commit;url/ref 前插 `--` 防选项注入 */
969
1025
  async function stageGit(source, ctx) {
1026
+ assertGitUrlSafe(source.url);
1027
+ if (source.ref?.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git ref must not start with "--": ${JSON.stringify(source.ref)}`);
970
1028
  const skillDir = `${ctx.stagingRoot}/repo`;
971
1029
  const args = [
972
1030
  "clone",
973
1031
  "--depth",
974
1032
  "1",
975
1033
  ...source.ref ? ["--branch", source.ref] : [],
1034
+ "--",
976
1035
  source.url,
977
1036
  skillDir
978
1037
  ];
@@ -1003,7 +1062,7 @@ function sha256Hex(data) {
1003
1062
  return createHash("sha256").update(data).digest("hex");
1004
1063
  }
1005
1064
  const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1006
- /** http 源:下载归档(可选 expectedSha256 校验包体)→ 解包 → 定位技能根 */
1065
+ /** http 源:下载归档(Content-Length + 流式累计上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
1007
1066
  async function stageHttp(source, ctx, expectedSha256) {
1008
1067
  const fetchImpl = ctx.fetchImpl ?? fetch;
1009
1068
  let res;
@@ -1013,18 +1072,18 @@ async function stageHttp(source, ctx, expectedSha256) {
1013
1072
  throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
1014
1073
  }
1015
1074
  if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
1016
- const data = new Uint8Array(await res.arrayBuffer());
1075
+ const data = await readResponseWithLimit(res, ctx.archiveLimits);
1017
1076
  if (expectedSha256 !== void 0) {
1018
1077
  const actual = sha256Hex(data);
1019
1078
  if (actual !== expectedSha256) throw new WebSkillError("INSTALL_FAILED", `Archive checksum mismatch: expected ${expectedSha256}, got ${actual}`);
1020
1079
  }
1021
1080
  const contentDir = `${ctx.stagingRoot}/content`;
1022
1081
  await ctx.fs.mkdir(contentDir);
1023
- if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir);
1082
+ if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir, ctx.archiveLimits);
1024
1083
  else {
1025
1084
  const archivePath = `${ctx.stagingRoot}/archive.tar`;
1026
1085
  await ctx.fs.writeBinary(archivePath, data);
1027
- await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir));
1086
+ await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir), ctx.archiveLimits);
1028
1087
  }
1029
1088
  const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
1030
1089
  if (await ctx.fs.exists(packFilePath)) return {
@@ -1056,16 +1115,27 @@ async function stageLocal(source, ctx) {
1056
1115
  source
1057
1116
  };
1058
1117
  }
1059
- /** npm 源:npm pack 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根 */
1118
+ /** npm 包名(@scope/name name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
1119
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
1120
+ const VERSION_RE = /^[a-z0-9._~^>=<x*+-]+$/i;
1121
+ function assertNpmSpecSafe(source) {
1122
+ const name = source.packageName;
1123
+ const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
1124
+ if (name.startsWith("--") || !PACKAGE_NAME_RE.test(name) && !isLocalPath) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package name: ${JSON.stringify(name)}`);
1125
+ 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)}`);
1126
+ }
1127
+ /** npm 源:npm pack → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
1060
1128
  async function stageNpm(source, ctx) {
1129
+ assertNpmSpecSafe(source);
1061
1130
  const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
1062
1131
  let stdout;
1063
1132
  try {
1064
1133
  ({stdout} = await execFileP("npm", [
1065
1134
  "pack",
1066
- spec,
1067
1135
  "--pack-destination",
1068
- ctx.stagingRoot
1136
+ ctx.stagingRoot,
1137
+ "--",
1138
+ spec
1069
1139
  ]));
1070
1140
  } catch (e) {
1071
1141
  throw commandFailed("npm", e);
@@ -1113,15 +1183,19 @@ async function readLockfile(fs, managedRoot) {
1113
1183
  async function upsertLockEntry(fs, managedRoot, name, entry) {
1114
1184
  const lockfile = await readLockfile(fs, managedRoot);
1115
1185
  lockfile.skills[name] = entry;
1116
- await fs.writeText(`${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1186
+ await writeLockfileAtomic(fs, managedRoot, lockfile);
1117
1187
  return lockfile;
1118
1188
  }
1119
1189
  async function removeLockEntry(fs, managedRoot, name) {
1120
1190
  const lockfile = await readLockfile(fs, managedRoot);
1121
1191
  delete lockfile.skills[name];
1122
- await fs.writeText(`${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1192
+ await writeLockfileAtomic(fs, managedRoot, lockfile);
1123
1193
  return lockfile;
1124
1194
  }
1195
+ /** 原子写:先写临时文件再 rename(进程崩溃不留下半写 lockfile) */
1196
+ async function writeLockfileAtomic(fs, managedRoot, lockfile) {
1197
+ await atomicWriteText(fs, `${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1198
+ }
1125
1199
  const EXCLUDED_FILES = /* @__PURE__ */ new Set([SKILL_MANIFEST_FILE, SKILLS_LOCKFILE]);
1126
1200
  /** 递归收集技能目录内的文件(相对路径,posix 分隔) */
1127
1201
  async function walkFiles(fs, root, prefix = "") {
@@ -1166,15 +1240,13 @@ async function readManifest(fs, skillRoot) {
1166
1240
  throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${e instanceof Error ? e.message : String(e)}`, e);
1167
1241
  }
1168
1242
  }
1169
- /** manifest.files 逐文件重算 sha256 比对(core 纯逻辑校验) */
1243
+ /** walk 实际目录与 manifest 比对:hash 不一致 / 清单外新增 / 清单内缺失三类全空才 ok */
1170
1244
  async function verifyIntegrity(fs, skillRoot) {
1171
1245
  const manifest = await readManifest(fs, skillRoot);
1172
1246
  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);
1247
+ const actualFiles = await walkFiles(fs, skillRoot);
1248
+ for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
1249
+ return verifyManifest(manifest, actualHashes, actualFiles);
1178
1250
  }
1179
1251
  const messageOf = (e) => e instanceof Error ? e.message : String(e);
1180
1252
  const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
@@ -1188,12 +1260,20 @@ var SkillManager = class {
1188
1260
  #fs;
1189
1261
  #fetchImpl;
1190
1262
  #schemaInference;
1263
+ #archiveLimits;
1264
+ #onChanged;
1191
1265
  #schemaInferer = new OxcSchemaInferer();
1192
1266
  constructor(deps) {
1193
1267
  this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
1194
1268
  this.#fs = deps.fs ?? new NodeFS();
1195
1269
  this.#fetchImpl = deps.fetchImpl;
1196
1270
  this.#schemaInference = deps.schemaInference ?? true;
1271
+ this.#archiveLimits = deps.archiveLimits;
1272
+ this.#onChanged = deps.onChanged;
1273
+ }
1274
+ /** 托管根目录(治理发布归档捕获等只读场景) */
1275
+ get managedRoot() {
1276
+ return this.#managedRoot;
1197
1277
  }
1198
1278
  async install(source, options) {
1199
1279
  const fs = this.#fs;
@@ -1204,7 +1284,8 @@ var SkillManager = class {
1204
1284
  const ctx = {
1205
1285
  fs,
1206
1286
  stagingRoot,
1207
- ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {}
1287
+ ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {},
1288
+ ...this.#archiveLimits ? { archiveLimits: this.#archiveLimits } : {}
1208
1289
  };
1209
1290
  let staged;
1210
1291
  switch (source.type) {
@@ -1268,6 +1349,7 @@ var SkillManager = class {
1268
1349
  throw e;
1269
1350
  }
1270
1351
  targetDir = void 0;
1352
+ this.#onChanged?.();
1271
1353
  return manifest;
1272
1354
  } catch (e) {
1273
1355
  if (targetDir) await removeDirQuiet(fs, targetDir);
@@ -1346,6 +1428,7 @@ var SkillManager = class {
1346
1428
  installed.length = 0;
1347
1429
  throw e;
1348
1430
  }
1431
+ this.#onChanged?.();
1349
1432
  return manifests;
1350
1433
  } catch (e) {
1351
1434
  for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
@@ -1381,6 +1464,7 @@ var SkillManager = class {
1381
1464
  try {
1382
1465
  await this.#fs.remove(targetDir, { recursive: true });
1383
1466
  await removeLockEntry(this.#fs, this.#managedRoot, name);
1467
+ this.#onChanged?.();
1384
1468
  } catch (e) {
1385
1469
  throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf(e)}`, e);
1386
1470
  }
@@ -1414,142 +1498,6 @@ var SkillManager = class {
1414
1498
  return options.outPath;
1415
1499
  }
1416
1500
  };
1417
- /** 解析 .env(简单 KEY=VALUE 行解析,不引依赖);文件缺失返回 undefined */
1418
- function readEnvVars(dotenvPath) {
1419
- let raw;
1420
- try {
1421
- raw = readFileSync(dotenvPath, "utf8");
1422
- } catch {
1423
- return;
1424
- }
1425
- const vars = /* @__PURE__ */ new Map();
1426
- for (const line of raw.split(/\r?\n/)) {
1427
- const trimmed = line.trim();
1428
- if (trimmed === "" || trimmed.startsWith("#")) continue;
1429
- const eq = trimmed.indexOf("=");
1430
- if (eq <= 0) continue;
1431
- const key = trimmed.slice(0, eq).trim();
1432
- let value = trimmed.slice(eq + 1).trim();
1433
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1434
- vars.set(key, value);
1435
- }
1436
- return vars;
1437
- }
1438
- /**
1439
- * 读取 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
1440
- */
1441
- function loadLlmConfigFromEnv(dotenvPath = ".env") {
1442
- const vars = readEnvVars(dotenvPath);
1443
- if (!vars) return void 0;
1444
- const baseUrl = vars.get("VITE_BASE_URL");
1445
- const apiKey = vars.get("VITE_API_KEY");
1446
- const model = vars.get("VITE_MODEL_NAME");
1447
- if (!baseUrl || !apiKey || !model) return void 0;
1448
- return {
1449
- baseUrl,
1450
- apiKey,
1451
- model
1452
- };
1453
- }
1454
- /** ANTHROPIC_API_KEY / ANTHROPIC_MODEL(可选 ANTHROPIC_BASE_URL);缺项返回 undefined */
1455
- function loadAnthropicConfigFromEnv(dotenvPath = ".env") {
1456
- const vars = readEnvVars(dotenvPath);
1457
- if (!vars) return void 0;
1458
- const apiKey = vars.get("ANTHROPIC_API_KEY");
1459
- const model = vars.get("ANTHROPIC_MODEL");
1460
- if (!apiKey || !model) return void 0;
1461
- const baseUrl = vars.get("ANTHROPIC_BASE_URL");
1462
- return {
1463
- apiKey,
1464
- model,
1465
- ...baseUrl ? { baseUrl } : {}
1466
- };
1467
- }
1468
- /** GOOGLE_API_KEY / GOOGLE_MODEL(可选 GOOGLE_BASE_URL);缺项返回 undefined */
1469
- function loadGoogleConfigFromEnv(dotenvPath = ".env") {
1470
- const vars = readEnvVars(dotenvPath);
1471
- if (!vars) return void 0;
1472
- const apiKey = vars.get("GOOGLE_API_KEY");
1473
- const model = vars.get("GOOGLE_MODEL");
1474
- if (!apiKey || !model) return void 0;
1475
- const baseUrl = vars.get("GOOGLE_BASE_URL");
1476
- return {
1477
- apiKey,
1478
- model,
1479
- ...baseUrl ? { baseUrl } : {}
1480
- };
1481
- }
1482
- let capabilitiesCache;
1483
- /**
1484
- * 探测 LLM 能力(结果模块级缓存):
1485
- * - available:GET /models 可达(与 checkAvailability 同语义)
1486
- * - nonStreaming:最小非流式 chat completion(max_tokens=1,短超时);
1487
- * HTTP 400/404 或明确不支持非流式的错误 → false
1488
- * - streaming 能力由 available 推断(SSE 解析在 SDK 侧实现)
1489
- * 环境变量 VITE_LLM_NON_STREAMING=0 可显式强制 nonStreaming=false(模拟流式-only 回归)。
1490
- */
1491
- function probeLlmCapabilities(config) {
1492
- capabilitiesCache ??= doProbeLlmCapabilities(config);
1493
- return capabilitiesCache;
1494
- }
1495
- async function doProbeLlmCapabilities(config) {
1496
- const baseUrl = config.baseUrl.replace(/\/+$/, "");
1497
- const headers = { authorization: `Bearer ${config.apiKey}` };
1498
- let available = false;
1499
- try {
1500
- available = (await fetch(`${baseUrl}/models`, {
1501
- headers,
1502
- signal: AbortSignal.timeout(1e4)
1503
- })).ok;
1504
- } catch {
1505
- available = false;
1506
- }
1507
- if (!available) return {
1508
- available: false,
1509
- nonStreaming: false,
1510
- reason: "endpoint unreachable or unauthorized"
1511
- };
1512
- if (process.env["VITE_LLM_NON_STREAMING"] === "0") return {
1513
- available: true,
1514
- nonStreaming: false,
1515
- reason: "non-streaming disabled via VITE_LLM_NON_STREAMING=0 (simulated)"
1516
- };
1517
- try {
1518
- const res = await fetch(`${baseUrl}/chat/completions`, {
1519
- method: "POST",
1520
- headers: {
1521
- ...headers,
1522
- "content-type": "application/json"
1523
- },
1524
- body: JSON.stringify({
1525
- model: config.model,
1526
- messages: [{
1527
- role: "user",
1528
- content: "OK"
1529
- }],
1530
- max_tokens: 1
1531
- }),
1532
- signal: AbortSignal.timeout(15e3)
1533
- });
1534
- const contentType = res.headers.get("content-type") ?? "";
1535
- if (res.ok && !contentType.includes("text/event-stream")) return {
1536
- available: true,
1537
- nonStreaming: true
1538
- };
1539
- const detail = (await res.text().catch(() => "")).slice(0, 200);
1540
- return {
1541
- available: true,
1542
- nonStreaming: false,
1543
- reason: `endpoint rejects non-streaming chat completions (HTTP ${res.status})${detail ? `: ${detail}` : ""}`
1544
- };
1545
- } catch (e) {
1546
- return {
1547
- available: true,
1548
- nonStreaming: false,
1549
- reason: `non-streaming probe failed: ${e instanceof Error ? e.message : String(e)}`
1550
- };
1551
- }
1552
- }
1553
1501
 
1554
1502
  //#endregion
1555
- export { NodeScriptExecutor as a, SkillManager as c, loadLlmConfigFromEnv as d, probeLlmCapabilities as f, NodeFS as i, loadAnthropicConfigFromEnv as l, FileArtifactStore as n, OxcSchemaInferer as o, readArchiveManifest as p, FileMemoryStore as r, SandboxedScriptExecutor as s, CliUiBridge as t, loadGoogleConfigFromEnv as u };
1503
+ export { NodeScriptExecutor as a, SkillManager as c, NodeFS as i, exportArchive as l, FileArtifactStore as n, OxcSchemaInferer as o, FileMemoryStore as r, SandboxedScriptExecutor as s, CliUiBridge as t, readArchiveManifest as u };