@webskill/sdk 0.1.2 → 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 { K as ScriptExecutionContext, Y as ToolDefinition, Z as ToolResult, _ as ExternalToolSource, _t as parseBridgeRequest, c as ApprovalScope, d as BridgeRequest, f as BridgeResponse, g as ExternalSkillProvider, k as NetworkPolicy, l as BridgeCapabilities, ot as bridgeError, q as ScriptExecutor, rt as WebSkillApi } from "./index-fjRF4H5o.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 normalizeToolContent, C as createWebSkillApi, D as isNetworkAllowed, O as mergeCatalogEntries, _ as RUN_SNAPSHOT_SCHEMA_VERSION, a as CapabilityApproval, b as bridgeError, c as FsMemoryStore, f as OpenAiCompatibleClient, i as AgentLoop, j as parseBridgeRequest, k as networkUrlHost, l as FsRunSnapshotStore, p as ProgressiveRouter, s as FsArtifactStore } from "./dist-DRsnVw6A.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 { A as normalizeToolContent, D as isNetworkAllowed, a as CapabilityApproval, b as bridgeError, c as FsMemoryStore, j as parseBridgeRequest, k as networkUrlHost, s as FsArtifactStore } from "./dist-DRsnVw6A.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);
@@ -1328,11 +1414,8 @@ var SkillManager = class {
1328
1414
  return options.outPath;
1329
1415
  }
1330
1416
  };
1331
- /**
1332
- * 解析 .env(简单 KEY=VALUE 行解析,不引依赖),
1333
- * 读取 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
1334
- */
1335
- function loadLlmConfigFromEnv(dotenvPath = ".env") {
1417
+ /** 解析 .env(简单 KEY=VALUE 行解析,不引依赖);文件缺失返回 undefined */
1418
+ function readEnvVars(dotenvPath) {
1336
1419
  let raw;
1337
1420
  try {
1338
1421
  raw = readFileSync(dotenvPath, "utf8");
@@ -1350,6 +1433,14 @@ function loadLlmConfigFromEnv(dotenvPath = ".env") {
1350
1433
  if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1351
1434
  vars.set(key, value);
1352
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;
1353
1444
  const baseUrl = vars.get("VITE_BASE_URL");
1354
1445
  const apiKey = vars.get("VITE_API_KEY");
1355
1446
  const model = vars.get("VITE_MODEL_NAME");
@@ -1360,6 +1451,34 @@ function loadLlmConfigFromEnv(dotenvPath = ".env") {
1360
1451
  model
1361
1452
  };
1362
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
+ }
1363
1482
  let capabilitiesCache;
1364
1483
  /**
1365
1484
  * 探测 LLM 能力(结果模块级缓存):
@@ -1433,4 +1552,4 @@ async function doProbeLlmCapabilities(config) {
1433
1552
  }
1434
1553
 
1435
1554
  //#endregion
1436
- export { NodeScriptExecutor as a, SkillManager as c, readArchiveManifest as d, NodeFS as i, loadLlmConfigFromEnv as l, FileArtifactStore as n, OxcSchemaInferer as o, FileMemoryStore as r, SandboxedScriptExecutor as s, CliUiBridge as t, probeLlmCapabilities as u };
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 };