@webskill/sdk 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,8 +16,11 @@ governance.
16
16
  (missing-parameter forms, confirmations, authorization prompts), and
17
17
  resumable interrupted runs.
18
18
  - **Script sandbox** — `worker_threads` sandbox for Node, Web Worker sandbox for
19
- the browser. Deny-by-default network policy, per-capability forced approval,
20
- timeouts kill runaway scripts.
19
+ the browser. Deny-by-default network policy, deny-by-default builtin-module
20
+ allowlist, per-capability forced approval, timeouts kill runaway scripts.
21
+ **Honest note:** these executors reduce the capability surface — they are NOT
22
+ a security boundary. Scripts share the host process/origin and bypasses
23
+ exist; never rely on them to isolate untrusted scripts.
21
24
  - **`navigator.webskill`** — a browser facade (`discover` / `read` / `validate` /
22
25
  `run` / `install` / `uninstall`) assembled explicitly in one call.
23
26
  - **MCP** — call page-provided tools and consume page-declared dynamic skills
package/dist/browser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, S as FileSystemProvider, W as VerifyResult, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, x as FileStat } from "./types-CgNC-oQu-Dq_A1yA-.js";
2
- import { $ as ToolDefinition, N as NetworkPolicy, X as ScriptExecutionContext, Z as ScriptExecutor, d as BridgeCapabilities, m as BridgeResponse, p as BridgeRequest, st as WebSkillApi, tt as ToolResult, u as ApprovalScope, ut as bridgeError, v as ExternalSkillProvider, xt as parseBridgeRequest, y as ExternalToolSource } from "./index-CqMuvcb2.js";
1
+ import { H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, S as FileSystemProvider, W as VerifyResult, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, 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, Z as ScriptExecutor, d as BridgeCapabilities, m as BridgeResponse, p as BridgeRequest, st as WebSkillApi, tt as ToolResult, u as ApprovalScope, ut as bridgeError, v as ExternalSkillProvider, xt as parseBridgeRequest, y as ExternalToolSource } from "./index-BSS3EWY-.js";
3
3
  //#region ../browser/dist/index.d.ts
4
4
  //#region src/fs/featureDetection.d.ts
5
5
  /** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
- import { E as verifyManifest, T as validateSkills, _ as isValidSkillName, b as parseSkillMarkdown, d as buildManifest, g as exportSkills, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, s as SkillDiscovery, u as buildCatalog, w as resolveInsideRoot, x as parseSkillPackManifest } from "./dist-DS1sfgHa.js";
2
- import { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DZobLFh6.js";
3
- import { n as MockLlmClient } from "./testing-CbM6rJ-E.js";
1
+ import { D as verifyManifest, E as validateSkills, S as parseSkillPackManifest, T as resolveInsideRoot, _ as exportSkills, d as buildCatalog, f as buildManifest, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, s as SkillDiscovery, v as isValidSkillName, x as parseSkillMarkdown } from "./dist-DvoBsChX.js";
2
+ import { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DAn07zHu.js";
3
+ import { n as MockLlmClient } from "./testing-S8SN9Lc6.js";
4
4
  import { unzipSync } from "fflate";
5
5
 
6
6
  //#region ../browser/dist/index.js
@@ -508,19 +508,31 @@ var BrowserSkillManager = class {
508
508
  const errors = report.issues.filter((i) => i.severity === "error");
509
509
  throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
510
510
  }
511
- targetDir = `${this.#managedRoot}/${name}`;
512
- if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
513
- await copyDir(fs, finalDir, targetDir);
514
- const manifest = await this.#createManifest(targetDir, {
511
+ const manifest = await this.#createManifest(finalDir, {
515
512
  name,
516
513
  ...version ? { version } : {},
517
514
  source
518
515
  });
519
- await this.#upsertLockEntry(name, {
520
- digest: manifest.integrity.digest,
521
- installedAt: manifest.installedAt,
522
- source
523
- });
516
+ targetDir = `${this.#managedRoot}/${name}`;
517
+ const backupDir = `${stagingRoot}/backup`;
518
+ const hadPrevious = await fs.exists(targetDir);
519
+ if (hadPrevious) await copyDir(fs, targetDir, backupDir);
520
+ let committed = false;
521
+ try {
522
+ if (hadPrevious) await fs.remove(targetDir, { recursive: true });
523
+ await copyDir(fs, finalDir, targetDir);
524
+ committed = true;
525
+ await this.#upsertLockEntry(name, {
526
+ digest: manifest.integrity.digest,
527
+ installedAt: manifest.installedAt,
528
+ source
529
+ });
530
+ } catch (e) {
531
+ if (committed) await removeDirQuiet(fs, targetDir);
532
+ if (hadPrevious) await copyDir(fs, backupDir, targetDir);
533
+ targetDir = void 0;
534
+ throw e;
535
+ }
524
536
  targetDir = void 0;
525
537
  return manifest;
526
538
  } catch (e) {
@@ -562,11 +574,7 @@ var BrowserSkillManager = class {
562
574
  }
563
575
  const manifests = [];
564
576
  for (const entry of pack.manifest.skills) {
565
- const targetDir = `${this.#managedRoot}/${entry.name}`;
566
- if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
567
- await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
568
- installed.push(targetDir);
569
- const manifest = await this.#createManifest(targetDir, {
577
+ const manifest = await this.#createManifest(`${finalRoot}/${entry.name}`, {
570
578
  name: entry.name,
571
579
  ...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
572
580
  source
@@ -574,11 +582,35 @@ var BrowserSkillManager = class {
574
582
  if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
575
583
  manifests.push(manifest);
576
584
  }
577
- for (const manifest of manifests) await this.#upsertLockEntry(manifest.name, {
578
- digest: manifest.integrity.digest,
579
- installedAt: manifest.installedAt,
580
- source
581
- });
585
+ const swapped = [];
586
+ try {
587
+ for (const manifest of manifests) {
588
+ const targetDir = `${this.#managedRoot}/${manifest.name}`;
589
+ const backupDir = `${stagingRoot}/backup/${manifest.name}`;
590
+ if (await fs.exists(targetDir)) {
591
+ await copyDir(fs, targetDir, backupDir);
592
+ await fs.remove(targetDir, { recursive: true });
593
+ }
594
+ await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
595
+ installed.push(targetDir);
596
+ swapped.push({
597
+ targetDir,
598
+ backupDir
599
+ });
600
+ }
601
+ for (const manifest of manifests) await this.#upsertLockEntry(manifest.name, {
602
+ digest: manifest.integrity.digest,
603
+ installedAt: manifest.installedAt,
604
+ source
605
+ });
606
+ } catch (e) {
607
+ for (const { targetDir, backupDir } of swapped) {
608
+ await removeDirQuiet(fs, targetDir);
609
+ if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
610
+ }
611
+ installed.length = 0;
612
+ throw e;
613
+ }
582
614
  return manifests;
583
615
  } catch (e) {
584
616
  for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
@@ -1,5 +1,5 @@
1
- import { E as verifyManifest, T as validateSkills, _ as isValidSkillName, b as parseSkillMarkdown, d as buildManifest, g as exportSkills, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, w as resolveInsideRoot, x as parseSkillPackManifest } from "./dist-DS1sfgHa.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-DZobLFh6.js";
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";
3
3
  import { unzipSync, zipSync } from "fflate";
4
4
  import { existsSync, promises, readFileSync } from "node:fs";
5
5
  import path from "node:path";
@@ -15,39 +15,73 @@ 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$3 = (p) => p.split("/").join(path.sep);
18
+ const toPlatform$4 = (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
- /** 基于 node:fs/promises 的 FileSystemProvider;接受 `/` 风格路径,写入自动创建父目录 */
21
+ /** realpath 最近现存祖先并拼回剩余段(写入目标尚不存在时的等价判定) */
22
+ async function realpathNearest(platformPath) {
23
+ let current = platformPath;
24
+ const rest = [];
25
+ for (;;) try {
26
+ const real = await promises.realpath(current);
27
+ return rest.length === 0 ? real : [real, ...rest].join(path.sep);
28
+ } catch (e) {
29
+ if (!isEnoent(e)) throw e;
30
+ rest.unshift(path.basename(current));
31
+ const parent = path.dirname(current);
32
+ if (parent === current) return platformPath;
33
+ current = parent;
34
+ }
35
+ }
36
+ /**
37
+ * 基于 node:fs/promises 的 FileSystemProvider;接受 `/` 风格路径,写入自动创建父目录。
38
+ * 可选 root 模式:构造传入 root 后,read/write 操作先做 realpath 包含校验
39
+ * (root 与目标都 realpath 后前缀比对),经符号链接逃逸 root → FS_PATH_OUTSIDE_ROOT。
40
+ */
22
41
  var NodeFS = class {
23
42
  kind = "node";
43
+ #root;
44
+ constructor(deps = {}) {
45
+ this.#root = deps.root;
46
+ }
47
+ /** root 模式:目标 realpath 必须落在 root realpath 前缀内(符号链接逃逸防护) */
48
+ async #assertContained(p) {
49
+ if (!this.#root) return;
50
+ const rootReal = await promises.realpath(toPlatform$4(this.#root));
51
+ const targetReal = await realpathNearest(toPlatform$4(p));
52
+ if (targetReal !== rootReal && !targetReal.startsWith(rootReal + path.sep)) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path escapes root via symbolic link: ${p} (resolves outside ${this.#root})`);
53
+ }
24
54
  async readText(p) {
55
+ await this.#assertContained(p);
25
56
  try {
26
- return await promises.readFile(toPlatform$3(p), "utf8");
57
+ return await promises.readFile(toPlatform$4(p), "utf8");
27
58
  } catch (e) {
28
59
  throw this.#mapError(e, p);
29
60
  }
30
61
  }
31
62
  async writeText(p, content) {
32
- const target = toPlatform$3(p);
63
+ await this.#assertContained(p);
64
+ const target = toPlatform$4(p);
33
65
  await promises.mkdir(path.dirname(target), { recursive: true });
34
66
  await promises.writeFile(target, content, "utf8");
35
67
  }
36
68
  async readBinary(p) {
69
+ await this.#assertContained(p);
37
70
  try {
38
- return await promises.readFile(toPlatform$3(p));
71
+ return await promises.readFile(toPlatform$4(p));
39
72
  } catch (e) {
40
73
  throw this.#mapError(e, p);
41
74
  }
42
75
  }
43
76
  async writeBinary(p, content) {
44
- const target = toPlatform$3(p);
77
+ await this.#assertContained(p);
78
+ const target = toPlatform$4(p);
45
79
  await promises.mkdir(path.dirname(target), { recursive: true });
46
80
  await promises.writeFile(target, content);
47
81
  }
48
82
  async exists(p) {
49
83
  try {
50
- await promises.access(toPlatform$3(p));
84
+ await promises.access(toPlatform$4(p));
51
85
  return true;
52
86
  } catch {
53
87
  return false;
@@ -55,7 +89,7 @@ var NodeFS = class {
55
89
  }
56
90
  async stat(p) {
57
91
  try {
58
- const s = await promises.stat(toPlatform$3(p));
92
+ const s = await promises.stat(toPlatform$4(p));
59
93
  return {
60
94
  path: toPosix(p),
61
95
  type: s.isDirectory() ? "directory" : "file",
@@ -69,21 +103,21 @@ var NodeFS = class {
69
103
  async list(p) {
70
104
  let dirents;
71
105
  try {
72
- dirents = await promises.readdir(toPlatform$3(p), { withFileTypes: true });
106
+ dirents = await promises.readdir(toPlatform$4(p), { withFileTypes: true });
73
107
  } catch (e) {
74
108
  throw this.#mapError(e, p);
75
109
  }
76
110
  return dirents.map((d) => ({
77
- path: toPosix(path.join(toPlatform$3(p), d.name)),
111
+ path: toPosix(path.join(toPlatform$4(p), d.name)),
78
112
  type: d.isDirectory() ? "directory" : "file"
79
113
  }));
80
114
  }
81
115
  async mkdir(p) {
82
- await promises.mkdir(toPlatform$3(p), { recursive: true });
116
+ await promises.mkdir(toPlatform$4(p), { recursive: true });
83
117
  }
84
118
  async remove(p, options) {
85
119
  try {
86
- await promises.rm(toPlatform$3(p), { recursive: options?.recursive ?? false });
120
+ await promises.rm(toPlatform$4(p), { recursive: options?.recursive ?? false });
87
121
  } catch (e) {
88
122
  throw this.#mapError(e, p);
89
123
  }
@@ -94,7 +128,7 @@ var NodeFS = class {
94
128
  }
95
129
  };
96
130
  const baseName$1 = (p) => p.split("/").pop() ?? p;
97
- const toPlatform$2 = (p) => p.split("/").join(path.sep);
131
+ const toPlatform$3 = (p) => p.split("/").join(path.sep);
98
132
  /**
99
133
  * 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
100
134
  * worker_threads 沙箱见 deferred-items D1。
@@ -114,7 +148,7 @@ var NodeScriptExecutor = class {
114
148
  return hasTs ? tsPath : jsPath;
115
149
  }
116
150
  async #loadModule(scriptPath) {
117
- return await import(`${pathToFileURL(toPlatform$2(scriptPath)).href}?t=${Date.now()}`);
151
+ return await import(`${pathToFileURL(toPlatform$3(scriptPath)).href}?t=${Date.now()}`);
118
152
  }
119
153
  async loadDefinition(skillRoot, scriptName) {
120
154
  const scriptPath = await this.#locateScript(skillRoot, scriptName);
@@ -223,14 +257,14 @@ var NodeScriptExecutor = class {
223
257
  }
224
258
  };
225
259
  const baseName = (p) => p.split("/").pop() ?? p;
226
- const toPlatform$1 = (p) => p.split("/").join(path.sep);
260
+ const toPlatform$2 = (p) => p.split("/").join(path.sep);
227
261
  const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
228
262
  /**
229
263
  * 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
230
264
  * runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
231
265
  */
232
266
  const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
233
- /** Worker 入口文件:src 为同目录 .ts;dist 为 dist/executor/ 下的 .js */
267
+ /** Worker 入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
234
268
  function workerEntryPath() {
235
269
  const dir = path.dirname(fileURLToPath(import.meta.url));
236
270
  const candidates = [
@@ -239,13 +273,17 @@ function workerEntryPath() {
239
273
  path.join(dir, "executor", "sandboxWorkerEntry.js")
240
274
  ];
241
275
  for (const candidate of candidates) if (existsSync(candidate)) return candidate;
242
- return candidates[0];
276
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker entry not found (tried: ${candidates.join(", ")})`);
243
277
  }
244
278
  /**
245
279
  * D1:worker_threads 沙箱脚本执行器。
246
280
  * 每次执行独立 Worker(resourceLimits + env 白名单),loadDefinition 同样在
247
281
  * Worker 内完成(禁止主进程 import 不可信脚本);超时 terminate(可杀同步死循环)。
248
282
  * 能力桥协议与浏览器同一来源(runtime/sandbox/bridgeProtocol)。
283
+ *
284
+ * 诚实标注:本执行器做的是**能力面收敛**(网络策略、模块 allowlist、资源限额、
285
+ * 超时强杀),**不是安全边界**——Worker 内脚本与宿主共享进程,仍有绕过手段;
286
+ * 禁止假定其可隔离不可信脚本。
249
287
  */
250
288
  var SandboxedScriptExecutor = class {
251
289
  #fs;
@@ -280,7 +318,7 @@ var SandboxedScriptExecutor = class {
280
318
  const skillName = baseName(skillRoot);
281
319
  const result = await this.#runWorker({
282
320
  mode: "load",
283
- scriptPath: toPlatform$1(scriptPath),
321
+ scriptPath: toPlatform$2(scriptPath),
284
322
  scriptName
285
323
  });
286
324
  if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
@@ -310,7 +348,7 @@ var SandboxedScriptExecutor = class {
310
348
  try {
311
349
  const result = await this.#runWorker({
312
350
  mode: "execute",
313
- scriptPath: toPlatform$1(scriptPath),
351
+ scriptPath: toPlatform$2(scriptPath),
314
352
  scriptName,
315
353
  skillName: context.skillName,
316
354
  runId: context.runId,
@@ -356,7 +394,8 @@ var SandboxedScriptExecutor = class {
356
394
  workerData: {
357
395
  ...workerData,
358
396
  networkPolicy: this.#networkPolicy,
359
- networkPolicyLib: NETWORK_POLICY_LIB
397
+ networkPolicyLib: NETWORK_POLICY_LIB,
398
+ allowedModules: this.#options.allowedModules ?? []
360
399
  },
361
400
  env,
362
401
  resourceLimits: this.#options.resourceLimits ?? {
@@ -776,7 +815,7 @@ var CliUiBridge = class {
776
815
  }
777
816
  };
778
817
  const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
779
- const toPlatform = (p) => p.split("/").join(path.sep);
818
+ const toPlatform$1 = (p) => p.split("/").join(path.sep);
780
819
  const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
781
820
  /**
782
821
  * zip 解包(fflate)。每个条目路径过 resolveInsideRoot:
@@ -803,15 +842,15 @@ async function extractTarFile(archivePath, destRoot) {
803
842
  try {
804
843
  const entryPaths = [];
805
844
  await tar.t({
806
- file: toPlatform(archivePath),
845
+ file: toPlatform$1(archivePath),
807
846
  onentry: (entry) => {
808
847
  entryPaths.push(entry.path);
809
848
  }
810
849
  });
811
850
  for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
812
851
  await tar.x({
813
- file: toPlatform(archivePath),
814
- cwd: toPlatform(destRoot),
852
+ file: toPlatform$1(archivePath),
853
+ cwd: toPlatform$1(destRoot),
815
854
  filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
816
855
  });
817
856
  } catch (e) {
@@ -830,8 +869,23 @@ async function listFiles(fs, root, prefix = "") {
830
869
  }
831
870
  return out;
832
871
  }
833
- /** 递归拷贝目录(含子目录与二进制文件) */
872
+ const toPlatform = (p) => p.split("/").join(path.sep);
873
+ /** lstat 扫描拒绝符号链接条目(与 tar 解包 filter 策略一致;虚拟 fs 路径不存在于真实 fs 时跳过) */
874
+ async function assertNoSymlinks(platformRoot) {
875
+ let entries;
876
+ try {
877
+ entries = await promises.readdir(platformRoot, { withFileTypes: true });
878
+ } catch {
879
+ return;
880
+ }
881
+ for (const entry of entries) {
882
+ if (entry.isSymbolicLink()) throw new WebSkillError("INSTALL_FAILED", `Refusing to copy symbolic link entry: ${platformRoot}${path.sep}${entry.name}`);
883
+ if (entry.isDirectory()) await assertNoSymlinks(`${platformRoot}${path.sep}${entry.name}`);
884
+ }
885
+ }
886
+ /** 递归拷贝目录(含子目录与二进制文件);真实 fs 上先做符号链接扫描 */
834
887
  async function copyDir(fs, from, to) {
888
+ await assertNoSymlinks(toPlatform(from));
835
889
  await fs.mkdir(to);
836
890
  for (const entry of await fs.list(from)) {
837
891
  const name = entry.path.split("/").pop() ?? "";
@@ -854,8 +908,8 @@ async function exportArchive(fs, skillRoot, options) {
854
908
  for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
855
909
  await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
856
910
  } else await tar.c({
857
- file: toPlatform(options.outPath),
858
- cwd: toPlatform(skillRoot)
911
+ file: toPlatform$1(options.outPath),
912
+ cwd: toPlatform$1(skillRoot)
859
913
  }, ["."]);
860
914
  return options.outPath;
861
915
  } catch (e) {
@@ -876,7 +930,7 @@ async function readArchiveManifest(fs, archivePath) {
876
930
  let manifestText;
877
931
  try {
878
932
  await tar.t({
879
- file: toPlatform(archivePath),
933
+ file: toPlatform$1(archivePath),
880
934
  onentry: (entry) => {
881
935
  if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
882
936
  const chunks = [];
@@ -970,7 +1024,7 @@ async function stageHttp(source, ctx, expectedSha256) {
970
1024
  else {
971
1025
  const archivePath = `${ctx.stagingRoot}/archive.tar`;
972
1026
  await ctx.fs.writeBinary(archivePath, data);
973
- await extractTarFile(toPlatform(archivePath), toPlatform(contentDir));
1027
+ await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir));
974
1028
  }
975
1029
  const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
976
1030
  if (await ctx.fs.exists(packFilePath)) return {
@@ -1021,7 +1075,7 @@ async function stageNpm(source, ctx) {
1021
1075
  if (!tarball || !await ctx.fs.exists(tgzPath)) throw new WebSkillError("INSTALL_FAILED", `npm pack output not found in staging directory: ${tarball ?? "(empty output)"}`);
1022
1076
  const pkgDir = `${ctx.stagingRoot}/pkg`;
1023
1077
  await ctx.fs.mkdir(pkgDir);
1024
- await extractTarFile(toPlatform(tgzPath), toPlatform(pkgDir));
1078
+ await extractTarFile(toPlatform$1(tgzPath), toPlatform$1(pkgDir));
1025
1079
  const pkgRoot = `${pkgDir}/package`;
1026
1080
  const skillDir = await ctx.fs.exists(`${pkgRoot}/skill/SKILL.md`) ? `${pkgRoot}/skill` : pkgRoot;
1027
1081
  let packageName = source.packageName;
@@ -1187,20 +1241,32 @@ var SkillManager = class {
1187
1241
  const errors = report.issues.filter((i) => i.severity === "error");
1188
1242
  throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
1189
1243
  }
1190
- targetDir = `${this.#managedRoot}/${name}`;
1191
- if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
1192
- await copyDir(fs, finalDir, targetDir);
1193
- await this.#inferSkillSchemas(targetDir);
1194
- const manifest = await createManifest(fs, targetDir, {
1244
+ await this.#inferSkillSchemas(finalDir);
1245
+ const manifest = await createManifest(fs, finalDir, {
1195
1246
  name,
1196
1247
  ...version ? { version } : {},
1197
1248
  source: staged.source
1198
1249
  });
1199
- await upsertLockEntry(fs, this.#managedRoot, name, {
1200
- digest: manifest.integrity.digest,
1201
- installedAt: manifest.installedAt,
1202
- source: staged.source
1203
- });
1250
+ targetDir = `${this.#managedRoot}/${name}`;
1251
+ const backupDir = `${stagingRoot}/backup`;
1252
+ const hadPrevious = await fs.exists(targetDir);
1253
+ if (hadPrevious) await copyDir(fs, targetDir, backupDir);
1254
+ let committed = false;
1255
+ try {
1256
+ if (hadPrevious) await fs.remove(targetDir, { recursive: true });
1257
+ await copyDir(fs, finalDir, targetDir);
1258
+ committed = true;
1259
+ await upsertLockEntry(fs, this.#managedRoot, name, {
1260
+ digest: manifest.integrity.digest,
1261
+ installedAt: manifest.installedAt,
1262
+ source: staged.source
1263
+ });
1264
+ } catch (e) {
1265
+ if (committed) await removeDirQuiet(fs, targetDir);
1266
+ if (hadPrevious) await copyDir(fs, backupDir, targetDir);
1267
+ targetDir = void 0;
1268
+ throw e;
1269
+ }
1204
1270
  targetDir = void 0;
1205
1271
  return manifest;
1206
1272
  } catch (e) {
@@ -1243,11 +1309,7 @@ var SkillManager = class {
1243
1309
  }
1244
1310
  const manifests = [];
1245
1311
  for (const entry of pack.manifest.skills) {
1246
- const targetDir = `${this.#managedRoot}/${entry.name}`;
1247
- if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
1248
- await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
1249
- installed.push(targetDir);
1250
- const manifest = await createManifest(fs, targetDir, {
1312
+ const manifest = await createManifest(fs, `${finalRoot}/${entry.name}`, {
1251
1313
  name: entry.name,
1252
1314
  ...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
1253
1315
  source
@@ -1255,11 +1317,35 @@ var SkillManager = class {
1255
1317
  if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
1256
1318
  manifests.push(manifest);
1257
1319
  }
1258
- for (const manifest of manifests) await upsertLockEntry(fs, this.#managedRoot, manifest.name, {
1259
- digest: manifest.integrity.digest,
1260
- installedAt: manifest.installedAt,
1261
- source
1262
- });
1320
+ const swapped = [];
1321
+ try {
1322
+ for (const manifest of manifests) {
1323
+ const targetDir = `${this.#managedRoot}/${manifest.name}`;
1324
+ const backupDir = `${stagingRoot}/backup/${manifest.name}`;
1325
+ if (await fs.exists(targetDir)) {
1326
+ await copyDir(fs, targetDir, backupDir);
1327
+ await fs.remove(targetDir, { recursive: true });
1328
+ }
1329
+ await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
1330
+ installed.push(targetDir);
1331
+ swapped.push({
1332
+ targetDir,
1333
+ backupDir
1334
+ });
1335
+ }
1336
+ for (const manifest of manifests) await upsertLockEntry(fs, this.#managedRoot, manifest.name, {
1337
+ digest: manifest.integrity.digest,
1338
+ installedAt: manifest.installedAt,
1339
+ source
1340
+ });
1341
+ } catch (e) {
1342
+ for (const { targetDir, backupDir } of swapped) {
1343
+ await removeDirQuiet(fs, targetDir);
1344
+ if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
1345
+ }
1346
+ installed.length = 0;
1347
+ throw e;
1348
+ }
1263
1349
  return manifests;
1264
1350
  } catch (e) {
1265
1351
  for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
@@ -1,4 +1,4 @@
1
- import { S as renderAvailableSkillsXml, T as validateSkills, b as parseSkillMarkdown, c as SkillReader, l as WebSkillError, s as SkillDiscovery, u as buildCatalog, w as resolveInsideRoot } from "./dist-DS1sfgHa.js";
1
+ import { C as renderAvailableSkillsXml, E as validateSkills, T as resolveInsideRoot, c as SkillReader, d as buildCatalog, l as WebSkillError, s as SkillDiscovery, u as assertSafePathSegment, x as parseSkillMarkdown } from "./dist-DvoBsChX.js";
2
2
  import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
3
3
 
4
4
  //#region ../runtime/dist/index.js
@@ -1577,6 +1577,7 @@ var AgentLoop = class {
1577
1577
  }
1578
1578
  /** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
1579
1579
  async #handleReadExternalSkill(skillKey, pathArg, state) {
1580
+ const failures = [];
1580
1581
  for (const provider of this.#deps.skillProviders ?? []) try {
1581
1582
  let name = skillKey;
1582
1583
  if (skillKey.startsWith("mcp://")) {
@@ -1610,10 +1611,12 @@ var AgentLoop = class {
1610
1611
  text: await provider.readFile(name, pathArg)
1611
1612
  }]
1612
1613
  };
1613
- } catch {
1614
+ } catch (e) {
1615
+ failures.push(messageOf$1(e));
1614
1616
  continue;
1615
1617
  }
1616
- return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}`);
1618
+ const detail = failures.length > 0 ? ` (provider errors: ${failures.join("; ")})` : "";
1619
+ return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
1617
1620
  }
1618
1621
  /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
1619
1622
  async #activateSkill(skillName, state, via, skillMdText) {
@@ -1635,7 +1638,9 @@ var AgentLoop = class {
1635
1638
  const rawAllowed = metadata["allowed-tools"];
1636
1639
  if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) allowedTools = rawAllowed.filter((e) => typeof e === "string");
1637
1640
  else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
1638
- } catch {}
1641
+ } catch (e) {
1642
+ state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf$1(e)}` });
1643
+ }
1639
1644
  const executor = this.#deps.executor;
1640
1645
  const loaded = [];
1641
1646
  if (executor) {
@@ -1654,7 +1659,9 @@ var AgentLoop = class {
1654
1659
  await this.#enrichDefinition(root, match[1], def, state);
1655
1660
  state.activatedTools.set(def.name, def);
1656
1661
  loaded.push(def.name);
1657
- } catch {}
1662
+ } catch (e) {
1663
+ state.trace.record("run.warning", { message: `Failed to load definition of script "${match[1]}" for skill "${skillName}": ${messageOf$1(e)}` });
1664
+ }
1658
1665
  }
1659
1666
  }
1660
1667
  let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
@@ -1924,10 +1931,12 @@ var WebSkillRuntime = class {
1924
1931
  if (!this.#catalogCache) await this.discover();
1925
1932
  const cache = this.#catalogCache;
1926
1933
  if (!cache) throw new Error("discover() did not populate the catalog cache");
1934
+ const providerFailures = [];
1927
1935
  const providerEntries = (await Promise.all((this.#deps.skillProviders ?? []).map(async (p) => {
1928
1936
  try {
1929
1937
  return await p.listSkills();
1930
- } catch {
1938
+ } catch (e) {
1939
+ providerFailures.push(e instanceof Error ? e.message : String(e));
1931
1940
  return [];
1932
1941
  }
1933
1942
  }))).flat();
@@ -1957,6 +1966,13 @@ var WebSkillRuntime = class {
1957
1966
  userPrompt,
1958
1967
  route
1959
1968
  });
1969
+ for (const failure of providerFailures) result.run.trace.push({
1970
+ id: `evt-provider-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1971
+ runId: result.run.id,
1972
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1973
+ type: "run.warning",
1974
+ message: `Skill provider listSkills() failed: ${failure}`
1975
+ });
1960
1976
  if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
1961
1977
  prompt: userPrompt,
1962
1978
  run: result.run
@@ -2279,6 +2295,7 @@ var FsArtifactStore = class {
2279
2295
  });
2280
2296
  }
2281
2297
  async listArtifacts(runId) {
2298
+ assertSafePathSegment(runId, "runId");
2282
2299
  const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
2283
2300
  if (!await this.#fs.exists(indexPath)) return [];
2284
2301
  const raw = await this.#fs.readText(indexPath);
@@ -2290,6 +2307,7 @@ var FsArtifactStore = class {
2290
2307
  }
2291
2308
  }
2292
2309
  #artifactPath(runId, artifactPath) {
2310
+ assertSafePathSegment(runId, "runId");
2293
2311
  return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
2294
2312
  }
2295
2313
  async #persist(input) {
@@ -174,8 +174,20 @@ function normalizePath(path) {
174
174
  return path.replace(/\\/g, "/").split("/").filter((s) => s !== "" && s !== ".").join("/");
175
175
  }
176
176
  /**
177
+ * 外部标识(runId、candidateId、skillName、versionId 等)作为单一路径段使用前的统一校验:
178
+ * 拒绝空串、`.`、`..`、含 `/` 或 `\`、含 `:`(Windows 盘符/ADS)。
179
+ * 违规抛 FS_PATH_OUTSIDE_ROOT(kind 用于错误消息定位,如 "runId")。
180
+ */
181
+ function assertSafePathSegment(segment, kind) {
182
+ if (segment === "" || segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\") || segment.includes(":")) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Invalid ${kind} (must be a single safe path segment): ${JSON.stringify(segment)}`);
183
+ }
184
+ /**
177
185
  * 将相对路径安全地解析到 root 之内。
178
186
  * 拒绝空路径、绝对路径(`/`、`\\`、`C:\` 形式)与任何 `..` 段。
187
+ *
188
+ * 注意:本函数仅做**词法**校验,不解析符号链接——root 内经符号链接指向
189
+ * 外部的目标无法被词法检查拦截(realpath 防护见 NodeFS root 模式与
190
+ * 安装管线的 lstat 符号链接扫描)。
179
191
  */
180
192
  function resolveInsideRoot(root, relativePath) {
181
193
  const fail = (reason) => {
@@ -587,4 +599,4 @@ const xmlRenderer = {
587
599
  };
588
600
 
589
601
  //#endregion
590
- export { renderCatalogJson as C, xmlRenderer as D, verifyManifest as E, renderAvailableSkillsXml as S, validateSkills as T, isValidSkillName as _, SKILL_NAME_PATTERN as a, parseSkillMarkdown as b, SkillReader as c, buildManifest as d, checkDependencyCycles as f, exportSkills as g, escapeXml as h, SKILL_NAME_MAX_LENGTH as i, WebSkillError as l, computeDigest as m, SKILLS_LOCKFILE as n, SKILL_PACK_FILE as o, checkSkillRules as p, SKILL_MANIFEST_FILE as r, SkillDiscovery as s, MemoryFS as t, buildCatalog as u, jsonRenderer as v, resolveInsideRoot as w, parseSkillPackManifest as x, normalizePath as y };
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 };