@webskill/sdk 0.1.5 → 0.2.1

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 { A as validateSkills, C as parseSkillPackManifest, D as resolveArchiveLimits, O as resolveInsideRoot, S as parseSkillMarkdown, i as SKILL_MANIFEST_FILE, j as verifyManifest, k as unzipWithLimits, p as buildManifest, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, v as exportSkills, w as readResponseWithLimit, y as isValidSkillName } from "./dist-fZFZaf43.js";
2
- import { M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, c as FsArtifactStore, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, o as CapabilityApproval } from "./dist-DuGN5x0v.js";
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-DrySSQ5R.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, realpathSync } from "node:fs";
5
6
  import path from "node:path";
6
7
  import { tmpdir } from "node:os";
7
8
  import { fileURLToPath, pathToFileURL } from "node:url";
8
9
  import { format, promisify } from "node:util";
9
10
  import { Worker } from "node:worker_threads";
10
- import { parseSync } from "oxc-parser";
11
+ import { mkdtemp, rm } from "node:fs/promises";
12
+ import { execFile, fork } from "node:child_process";
11
13
  import { createInterface } from "node:readline/promises";
12
- import { mkdtemp } from "node:fs/promises";
13
- import * as tar from "tar";
14
- 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$3 = (p) => p.split("/").join(path.sep);
17
+ const toPlatform$4 = (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$3(this.#root));
51
- const targetReal = await realpathNearest(toPlatform$3(p));
49
+ const rootReal = await promises.realpath(toPlatform$4(this.#root));
50
+ const targetReal = await realpathNearest(toPlatform$4(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$3(p), "utf8");
56
+ return await promises.readFile(toPlatform$4(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$3(p);
63
+ const target = toPlatform$4(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$3(p));
70
+ return await promises.readFile(toPlatform$4(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$3(p);
77
+ const target = toPlatform$4(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$3(p));
83
+ await promises.access(toPlatform$4(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$3(p));
91
+ const s = await promises.stat(toPlatform$4(p));
93
92
  return {
94
93
  path: toPosix(p),
95
94
  type: s.isDirectory() ? "directory" : "file",
@@ -103,31 +102,39 @@ var NodeFS = class {
103
102
  async list(p) {
104
103
  let dirents;
105
104
  try {
106
- dirents = await promises.readdir(toPlatform$3(p), { withFileTypes: true });
105
+ dirents = await promises.readdir(toPlatform$4(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$3(p), d.name)),
110
+ path: toPosix(path.join(toPlatform$4(p), d.name)),
112
111
  type: d.isDirectory() ? "directory" : "file"
113
112
  }));
114
113
  }
115
114
  async mkdir(p) {
116
- await promises.mkdir(toPlatform$3(p), { recursive: true });
115
+ await promises.mkdir(toPlatform$4(p), { recursive: true });
117
116
  }
118
117
  async remove(p, options) {
119
118
  try {
120
- await promises.rm(toPlatform$3(p), { recursive: options?.recursive ?? false });
119
+ await promises.rm(toPlatform$4(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$4(to)), { recursive: true });
126
+ try {
127
+ await promises.rename(toPlatform$4(from), toPlatform$4(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
- const baseName$1 = (p) => p.split("/").pop() ?? p;
137
+ const baseName$2 = (p) => p.split("/").pop() ?? p;
131
138
  /**
132
139
  * 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
133
140
  * worker_threads 沙箱见 deferred-items D1。
@@ -166,7 +173,7 @@ var NodeScriptExecutor = class {
166
173
  }
167
174
  async loadDefinition(skillRoot, scriptName) {
168
175
  const scriptPath = await this.#locateScript(skillRoot, scriptName);
169
- const skillName = baseName$1(skillRoot);
176
+ const skillName = baseName$2(skillRoot);
170
177
  let module;
171
178
  try {
172
179
  module = await this.#loadModule(scriptPath);
@@ -270,14 +277,14 @@ var NodeScriptExecutor = class {
270
277
  };
271
278
  }
272
279
  };
273
- const baseName = (p) => p.split("/").pop() ?? p;
274
- const toPlatform$2 = (p) => p.split("/").join(path.sep);
275
- const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
280
+ const baseName$1 = (p) => p.split("/").pop() ?? p;
281
+ const toPlatform$3 = (p) => p.split("/").join(path.sep);
282
+ const messageOf$6 = (e) => e instanceof Error ? e.message : String(e);
276
283
  /**
277
284
  * 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
278
285
  * runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
279
286
  */
280
- const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
287
+ const NETWORK_POLICY_LIB$1 = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
281
288
  /** Worker 入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
282
289
  function workerEntryPath() {
283
290
  const dir = path.dirname(fileURLToPath(import.meta.url));
@@ -329,10 +336,10 @@ var SandboxedScriptExecutor = class {
329
336
  }
330
337
  async loadDefinition(skillRoot, scriptName) {
331
338
  const scriptPath = await this.#locateScript(skillRoot, scriptName);
332
- const skillName = baseName(skillRoot);
339
+ const skillName = baseName$1(skillRoot);
333
340
  const result = await this.#runWorker({
334
341
  mode: "load",
335
- scriptPath: toPlatform$2(scriptPath),
342
+ scriptPath: toPlatform$3(scriptPath),
336
343
  scriptName,
337
344
  scriptSource: await this.#fs.readText(scriptPath)
338
345
  });
@@ -356,14 +363,14 @@ var SandboxedScriptExecutor = class {
356
363
  content: [],
357
364
  error: {
358
365
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
359
- message: messageOf$5(e)
366
+ message: messageOf$6(e)
360
367
  }
361
368
  };
362
369
  }
363
370
  try {
364
371
  const result = await this.#runWorker({
365
372
  mode: "execute",
366
- scriptPath: toPlatform$2(scriptPath),
373
+ scriptPath: toPlatform$3(scriptPath),
367
374
  scriptName,
368
375
  skillName: context.skillName,
369
376
  runId: context.runId,
@@ -396,7 +403,7 @@ var SandboxedScriptExecutor = class {
396
403
  content: [],
397
404
  error: {
398
405
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
399
- message: messageOf$5(e)
406
+ message: messageOf$6(e)
400
407
  }
401
408
  };
402
409
  }
@@ -410,7 +417,7 @@ var SandboxedScriptExecutor = class {
410
417
  workerData: {
411
418
  ...workerData,
412
419
  networkPolicy: this.#networkPolicy,
413
- networkPolicyLib: NETWORK_POLICY_LIB,
420
+ networkPolicyLib: NETWORK_POLICY_LIB$1,
414
421
  allowedModules: this.#options.allowedModules ?? []
415
422
  },
416
423
  env,
@@ -447,7 +454,7 @@ var SandboxedScriptExecutor = class {
447
454
  respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
448
455
  return;
449
456
  }
450
- onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$5(e))));
457
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$6(e))));
451
458
  return;
452
459
  }
453
460
  if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
@@ -459,6 +466,328 @@ var SandboxedScriptExecutor = class {
459
466
  });
460
467
  }
461
468
  /** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
469
+ async #handleBridge(request, context) {
470
+ const gate = async (capability, message, details) => {
471
+ const decision = await this.#approval.authorize({
472
+ runId: context.runId,
473
+ capability,
474
+ mode: this.#capabilities[capability],
475
+ message,
476
+ ...details !== void 0 ? { details } : {}
477
+ });
478
+ if (decision === "allowed") return void 0;
479
+ return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
480
+ };
481
+ try {
482
+ switch (request.kind) {
483
+ case "readReference": {
484
+ const denied = await gate("readReference", `Script "${context.skillName}" wants to read reference "${request.path}"`, { path: request.path });
485
+ if (denied) return denied;
486
+ return {
487
+ id: request.id,
488
+ ok: true,
489
+ value: await context.readReference(request.path)
490
+ };
491
+ }
492
+ case "writeArtifact": {
493
+ const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
494
+ path: request.path,
495
+ mimeType: request.mimeType
496
+ });
497
+ if (denied) return denied;
498
+ const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
499
+ const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
500
+ return {
501
+ id: request.id,
502
+ ok: true,
503
+ value: artifact
504
+ };
505
+ }
506
+ case "confirm": {
507
+ if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
508
+ const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
509
+ if (denied) return denied;
510
+ return {
511
+ id: request.id,
512
+ ok: true,
513
+ value: await context.confirm(request.message)
514
+ };
515
+ }
516
+ }
517
+ } catch (e) {
518
+ return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf$6(e));
519
+ }
520
+ }
521
+ };
522
+ const baseName = (p) => p.split("/").pop() ?? p;
523
+ const toPlatform$2 = (p) => p.split("/").join(path.sep);
524
+ const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
525
+ const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
526
+ const readAllow = (p) => [`--allow-fs-read=${p}`, `--allow-fs-read=${realpathSync(p)}`];
527
+ const writeAllow = (p) => [`--allow-fs-write=${p}`, `--allow-fs-write=${realpathSync(p)}`];
528
+ /** 子进程入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
529
+ function processEntryPath() {
530
+ const dir = path.dirname(fileURLToPath(import.meta.url));
531
+ const candidates = [
532
+ path.join(dir, "processSandboxEntry.js"),
533
+ path.join(dir, "processSandboxEntry.ts"),
534
+ path.join(dir, "executor", "processSandboxEntry.js")
535
+ ];
536
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
537
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", `Process sandbox entry not found (tried: ${candidates.join(", ")})`);
538
+ }
539
+ /**
540
+ * child_process.fork + --permission 进程沙箱(真实进程隔离)。
541
+ * 每个子进程以 `--permission --allow-fs-read=<入口目录> --allow-fs-read=<skillRoot>
542
+ * --allow-fs-write=<artifactDir>` 启动:fs 维度由权限模型管控(experimental),
543
+ * 子进程/Worker/addons 默认禁;网络无权限维度仍靠 fetch/WebSocket patch。
544
+ * 能力桥协议与 worker_threads 沙箱同一形态(readReference/writeArtifact/confirm + 授权)。
545
+ * 温池(默认 2):同 skillRoot 复用子进程;执行后 kill 并补位重生;并发上限即池大小。
546
+ * 超时 kill(同步死循环可杀);非零退出码 → TOOL_EXECUTION_FAILED。
547
+ */
548
+ var ProcessSandboxExecutor = class {
549
+ #fs;
550
+ #options;
551
+ #capabilities;
552
+ #networkPolicy;
553
+ #approval;
554
+ #slots = [];
555
+ #waiters = [];
556
+ constructor(fs, options = {}) {
557
+ this.#fs = fs;
558
+ this.#options = options;
559
+ this.#capabilities = {
560
+ readReference: options.capabilities?.readReference ?? true,
561
+ writeArtifact: options.capabilities?.writeArtifact ?? true,
562
+ confirm: options.capabilities?.confirm ?? true
563
+ };
564
+ this.#networkPolicy = options.networkPolicy ?? "deny-all";
565
+ this.#approval = new CapabilityApproval({
566
+ ...options.uiBridge ? { uiBridge: options.uiBridge } : {},
567
+ ...options.approvalScope ? { scope: options.approvalScope } : {}
568
+ });
569
+ }
570
+ get poolSize() {
571
+ return this.#options.poolSize ?? 2;
572
+ }
573
+ /** 池全部子进程销毁(测试收尾/进程退出前调用) */
574
+ async dispose() {
575
+ for (const slot of this.#slots) {
576
+ slot.child.kill();
577
+ await rm(slot.artifactDir, {
578
+ recursive: true,
579
+ force: true
580
+ }).catch(() => void 0);
581
+ }
582
+ this.#slots = [];
583
+ this.#waiters = [];
584
+ }
585
+ async #locateScript(skillRoot, scriptName) {
586
+ const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
587
+ const jsPath = `${skillRoot}/scripts/${scriptName}.js`;
588
+ const [hasTs, hasJs] = await Promise.all([this.#fs.exists(tsPath), this.#fs.exists(jsPath)]);
589
+ if (hasTs && hasJs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
590
+ if (!hasTs && !hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
591
+ return hasTs ? tsPath : jsPath;
592
+ }
593
+ /** 取一个可用子进程(同 key 复用 / 淘汰 idle 重生 / 扩容 / 排队) */
594
+ async #acquire(key) {
595
+ for (;;) {
596
+ const idle = this.#slots.filter((s) => !s.busy);
597
+ const match = idle.find((s) => s.key === key);
598
+ if (match) {
599
+ match.busy = true;
600
+ return match;
601
+ }
602
+ if (idle.length > 0) {
603
+ const victim = idle[0];
604
+ victim.child.kill();
605
+ await rm(victim.artifactDir, {
606
+ recursive: true,
607
+ force: true
608
+ }).catch(() => void 0);
609
+ this.#slots.splice(this.#slots.indexOf(victim), 1);
610
+ }
611
+ if (this.#slots.length < this.poolSize) {
612
+ const slot = await this.#spawnSlot(key);
613
+ slot.busy = true;
614
+ this.#slots.push(slot);
615
+ return slot;
616
+ }
617
+ await new Promise((resolve) => this.#waiters.push(resolve));
618
+ }
619
+ }
620
+ /** 执行后回收:kill 旧子进程,补位重生同 key 新子进程(温池保持),唤醒排队 */
621
+ #recycle(slot) {
622
+ slot.child.kill();
623
+ const index = this.#slots.indexOf(slot);
624
+ if (index >= 0) this.#slots.splice(index, 1);
625
+ rm(slot.artifactDir, {
626
+ recursive: true,
627
+ force: true
628
+ }).catch(() => void 0);
629
+ this.#spawnSlot(slot.key).then((fresh) => {
630
+ this.#slots.push(fresh);
631
+ }).catch(() => void 0).finally(() => {
632
+ const waiter = this.#waiters.shift();
633
+ if (waiter) waiter();
634
+ });
635
+ }
636
+ async #spawnSlot(key) {
637
+ const artifactDir = await mkdtemp(path.join(tmpdir(), "webskill-psbx-out-")).then((d) => d.split(path.sep).join("/"));
638
+ const entry = processEntryPath();
639
+ return {
640
+ key,
641
+ child: fork(entry, [], {
642
+ execArgv: [
643
+ "--permission",
644
+ ...readAllow(path.dirname(entry)),
645
+ ...readAllow(toPlatform$2(key)),
646
+ ...readAllow(artifactDir),
647
+ ...writeAllow(artifactDir)
648
+ ],
649
+ silent: false
650
+ }),
651
+ busy: false,
652
+ artifactDir
653
+ };
654
+ }
655
+ async loadDefinition(skillRoot, scriptName) {
656
+ const scriptPath = await this.#locateScript(skillRoot, scriptName);
657
+ const skillName = baseName(skillRoot);
658
+ const slot = await this.#acquire(toPlatform$2(skillRoot));
659
+ try {
660
+ const response = await this.#runTask(slot, {
661
+ mode: "load",
662
+ scriptPath: toPlatform$2(scriptPath),
663
+ scriptName,
664
+ scriptSource: await this.#fs.readText(scriptPath),
665
+ scratchDir: slot.artifactDir
666
+ }, 3e4);
667
+ if (!response.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${response.error?.message ?? "unknown error"}`);
668
+ return {
669
+ name: `${skillName}__${scriptName}`,
670
+ description: response.definition?.description,
671
+ inputSchema: response.definition?.inputSchema,
672
+ source: "script",
673
+ skillName
674
+ };
675
+ } finally {
676
+ this.#recycle(slot);
677
+ }
678
+ }
679
+ async execute(input) {
680
+ const { skillRoot, scriptName, args, context, timeoutMs } = input;
681
+ let scriptPath;
682
+ try {
683
+ scriptPath = await this.#locateScript(skillRoot, scriptName);
684
+ } catch (e) {
685
+ return {
686
+ ok: false,
687
+ content: [],
688
+ error: {
689
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
690
+ message: messageOf$5(e)
691
+ }
692
+ };
693
+ }
694
+ const slot = await this.#acquire(toPlatform$2(skillRoot));
695
+ try {
696
+ const result = await this.#runTask(slot, {
697
+ mode: "execute",
698
+ scriptPath: toPlatform$2(scriptPath),
699
+ scriptName,
700
+ skillName: context.skillName,
701
+ runId: context.runId,
702
+ args,
703
+ scriptSource: await this.#fs.readText(scriptPath),
704
+ scratchDir: slot.artifactDir
705
+ }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
706
+ if (!result.ok) {
707
+ const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
708
+ return {
709
+ ok: false,
710
+ content: [],
711
+ error: {
712
+ code: result.error?.code ?? "TOOL_EXECUTION_FAILED",
713
+ message: `${result.error?.message ?? "script execution failed"}${stderrSummary}`
714
+ }
715
+ };
716
+ }
717
+ const content = normalizeToolContent(result.value);
718
+ if (result.stdout?.length) content.push({
719
+ type: "text",
720
+ text: result.stdout.join("\n")
721
+ });
722
+ return {
723
+ ok: true,
724
+ content
725
+ };
726
+ } catch (e) {
727
+ return {
728
+ ok: false,
729
+ content: [],
730
+ error: {
731
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
732
+ message: messageOf$5(e)
733
+ }
734
+ };
735
+ } finally {
736
+ this.#recycle(slot);
737
+ }
738
+ }
739
+ /** 在子进程上跑一个任务;超时 kill;非零退出码 → TOOL_EXECUTION_FAILED */
740
+ #runTask(slot, task, timeoutMs, onBridge, onNetworkBlocked) {
741
+ const { child } = slot;
742
+ return new Promise((resolve, reject) => {
743
+ let settled = false;
744
+ const timer = setTimeout(() => {
745
+ if (settled) return;
746
+ settled = true;
747
+ child.kill();
748
+ reject(new WebSkillError("RUN_TIMEOUT", `Process sandbox task timed out after ${timeoutMs}ms (child killed)`));
749
+ }, timeoutMs);
750
+ const done = (fn) => {
751
+ if (settled) return;
752
+ settled = true;
753
+ clearTimeout(timer);
754
+ fn();
755
+ };
756
+ child.on("message", (msg) => {
757
+ if (msg?.type === "network-blocked") {
758
+ if (typeof msg.host === "string") onNetworkBlocked?.(msg.host);
759
+ return;
760
+ }
761
+ if (msg?.type === "bridge" && onBridge) {
762
+ const request = parseBridgeRequest(msg.request);
763
+ const respond = (response) => child.send({
764
+ type: "bridge-response",
765
+ response
766
+ });
767
+ if (!request) {
768
+ respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
769
+ return;
770
+ }
771
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$5(e))));
772
+ return;
773
+ }
774
+ if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
775
+ });
776
+ child.on("error", (e) => done(() => reject(e)));
777
+ child.on("exit", (code) => {
778
+ if (!settled) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox child exited ${code === null ? "by signal" : `with code ${code}`} without producing a result`)));
779
+ });
780
+ child.send({
781
+ type: "task",
782
+ task: {
783
+ ...task,
784
+ networkPolicy: this.#networkPolicy,
785
+ networkPolicyLib: NETWORK_POLICY_LIB
786
+ }
787
+ });
788
+ });
789
+ }
790
+ /** 能力桥宿主侧处理:与 worker_threads 沙箱同一判定(runtime/sandbox/approval 单一来源) */
462
791
  async #handleBridge(request, context) {
463
792
  const gate = async (capability, message, details) => {
464
793
  const decision = await this.#approval.authorize({
@@ -512,6 +841,16 @@ var SandboxedScriptExecutor = class {
512
841
  }
513
842
  }
514
843
  };
844
+ const require_ = createRequire(import.meta.url);
845
+ let parseSyncCache;
846
+ function loadParseSync() {
847
+ if (!parseSyncCache) try {
848
+ parseSyncCache = require_("oxc-parser").parseSync;
849
+ } catch (e) {
850
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"oxc-parser\" package is required for schema inference; install it first (npm i oxc-parser)", e);
851
+ }
852
+ return parseSyncCache;
853
+ }
515
854
  const PLACEHOLDER_PREFIX = "Inferred placeholder (unsupported type: ";
516
855
  const placeholder = (text) => ({
517
856
  type: "string",
@@ -561,7 +900,7 @@ var TypeMapper = class {
561
900
  };
562
901
  return placeholder(this.#text(t));
563
902
  }
564
- #union(t, seen) {
903
+ #union(t, _seen) {
565
904
  const values = [];
566
905
  for (const member of t.types ?? []) {
567
906
  const value = member.literal?.value ?? member.value;
@@ -661,7 +1000,7 @@ var OxcSchemaInferer = class {
661
1000
  let program;
662
1001
  let comments;
663
1002
  try {
664
- const result = parseSync(options?.fileName ?? "script.ts", source);
1003
+ const result = loadParseSync()(options?.fileName ?? "script.ts", source);
665
1004
  if (result.errors?.length > 0) return void 0;
666
1005
  program = result.program;
667
1006
  comments = result.comments ?? [];
@@ -677,7 +1016,7 @@ var OxcSchemaInferer = class {
677
1016
  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];
678
1017
  if (!jsdoc) return void 0;
679
1018
  const reparse = (typeText) => {
680
- const r = parseSync("__t.ts", `type __T = ${typeText};`);
1019
+ const r = loadParseSync()("__t.ts", `type __T = ${typeText};`);
681
1020
  if (r.errors?.length > 0) return void 0;
682
1021
  return r.program.body.find((n) => n.type === "TSTypeAliasDeclaration")?.typeAnnotation;
683
1022
  };
@@ -833,6 +1172,13 @@ var CliUiBridge = class {
833
1172
  const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
834
1173
  const toPlatform$1 = (p) => p.split("/").join(path.sep);
835
1174
  const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
1175
+ let tarModule$1;
1176
+ async function loadTar$1() {
1177
+ tarModule$1 ??= await import("tar").catch((e) => {
1178
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
1179
+ });
1180
+ return tarModule$1;
1181
+ }
836
1182
  /**
837
1183
  * zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
838
1184
  * 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
@@ -853,7 +1199,8 @@ async function extractTarFile(archivePath, destRoot, limits) {
853
1199
  try {
854
1200
  const entryPaths = [];
855
1201
  let total = 0;
856
- await tar.t({
1202
+ const tarMod = await loadTar$1();
1203
+ await tarMod.t({
857
1204
  file: toPlatform$1(archivePath),
858
1205
  onentry: (entry) => {
859
1206
  entryPaths.push(entry.path);
@@ -864,7 +1211,7 @@ async function extractTarFile(archivePath, destRoot, limits) {
864
1211
  }
865
1212
  });
866
1213
  for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
867
- await tar.x({
1214
+ await tarMod.x({
868
1215
  file: toPlatform$1(archivePath),
869
1216
  cwd: toPlatform$1(destRoot),
870
1217
  filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
@@ -916,6 +1263,13 @@ async function removeDirQuiet(fs, dir) {
916
1263
  } catch {}
917
1264
  }
918
1265
  const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
1266
+ let tarModule;
1267
+ async function loadTar() {
1268
+ tarModule ??= await import("tar").catch((e) => {
1269
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
1270
+ });
1271
+ return tarModule;
1272
+ }
919
1273
  /** 导出技能目录全部文件(含 manifest)为 zip/tar 归档,返回 outPath */
920
1274
  async function exportArchive(fs, skillRoot, options) {
921
1275
  try {
@@ -923,7 +1277,7 @@ async function exportArchive(fs, skillRoot, options) {
923
1277
  const files = {};
924
1278
  for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
925
1279
  await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
926
- } else await tar.c({
1280
+ } else await (await loadTar()).c({
927
1281
  file: toPlatform$1(options.outPath),
928
1282
  cwd: toPlatform$1(skillRoot)
929
1283
  }, ["."]);
@@ -945,7 +1299,7 @@ async function readArchiveManifest(fs, archivePath) {
945
1299
  }
946
1300
  let manifestText;
947
1301
  try {
948
- await tar.t({
1302
+ await (await loadTar()).t({
949
1303
  file: toPlatform$1(archivePath),
950
1304
  onentry: (entry) => {
951
1305
  if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
@@ -1151,15 +1505,19 @@ async function readLockfile(fs, managedRoot) {
1151
1505
  async function upsertLockEntry(fs, managedRoot, name, entry) {
1152
1506
  const lockfile = await readLockfile(fs, managedRoot);
1153
1507
  lockfile.skills[name] = entry;
1154
- await fs.writeText(`${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1508
+ await writeLockfileAtomic(fs, managedRoot, lockfile);
1155
1509
  return lockfile;
1156
1510
  }
1157
1511
  async function removeLockEntry(fs, managedRoot, name) {
1158
1512
  const lockfile = await readLockfile(fs, managedRoot);
1159
1513
  delete lockfile.skills[name];
1160
- await fs.writeText(`${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1514
+ await writeLockfileAtomic(fs, managedRoot, lockfile);
1161
1515
  return lockfile;
1162
1516
  }
1517
+ /** 原子写:先写临时文件再 rename(进程崩溃不留下半写 lockfile) */
1518
+ async function writeLockfileAtomic(fs, managedRoot, lockfile) {
1519
+ await atomicWriteText(fs, `${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1520
+ }
1163
1521
  const EXCLUDED_FILES = /* @__PURE__ */ new Set([SKILL_MANIFEST_FILE, SKILLS_LOCKFILE]);
1164
1522
  /** 递归收集技能目录内的文件(相对路径,posix 分隔) */
1165
1523
  async function walkFiles(fs, root, prefix = "") {
@@ -1235,6 +1593,10 @@ var SkillManager = class {
1235
1593
  this.#archiveLimits = deps.archiveLimits;
1236
1594
  this.#onChanged = deps.onChanged;
1237
1595
  }
1596
+ /** 托管根目录(治理发布归档捕获等只读场景) */
1597
+ get managedRoot() {
1598
+ return this.#managedRoot;
1599
+ }
1238
1600
  async install(source, options) {
1239
1601
  const fs = this.#fs;
1240
1602
  const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-install-"))).split(path.sep).join("/");
@@ -1458,142 +1820,6 @@ var SkillManager = class {
1458
1820
  return options.outPath;
1459
1821
  }
1460
1822
  };
1461
- /** 解析 .env(简单 KEY=VALUE 行解析,不引依赖);文件缺失返回 undefined */
1462
- function readEnvVars(dotenvPath) {
1463
- let raw;
1464
- try {
1465
- raw = readFileSync(dotenvPath, "utf8");
1466
- } catch {
1467
- return;
1468
- }
1469
- const vars = /* @__PURE__ */ new Map();
1470
- for (const line of raw.split(/\r?\n/)) {
1471
- const trimmed = line.trim();
1472
- if (trimmed === "" || trimmed.startsWith("#")) continue;
1473
- const eq = trimmed.indexOf("=");
1474
- if (eq <= 0) continue;
1475
- const key = trimmed.slice(0, eq).trim();
1476
- let value = trimmed.slice(eq + 1).trim();
1477
- if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1478
- vars.set(key, value);
1479
- }
1480
- return vars;
1481
- }
1482
- /**
1483
- * 读取 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
1484
- */
1485
- function loadLlmConfigFromEnv(dotenvPath = ".env") {
1486
- const vars = readEnvVars(dotenvPath);
1487
- if (!vars) return void 0;
1488
- const baseUrl = vars.get("VITE_BASE_URL");
1489
- const apiKey = vars.get("VITE_API_KEY");
1490
- const model = vars.get("VITE_MODEL_NAME");
1491
- if (!baseUrl || !apiKey || !model) return void 0;
1492
- return {
1493
- baseUrl,
1494
- apiKey,
1495
- model
1496
- };
1497
- }
1498
- /** ANTHROPIC_API_KEY / ANTHROPIC_MODEL(可选 ANTHROPIC_BASE_URL);缺项返回 undefined */
1499
- function loadAnthropicConfigFromEnv(dotenvPath = ".env") {
1500
- const vars = readEnvVars(dotenvPath);
1501
- if (!vars) return void 0;
1502
- const apiKey = vars.get("ANTHROPIC_API_KEY");
1503
- const model = vars.get("ANTHROPIC_MODEL");
1504
- if (!apiKey || !model) return void 0;
1505
- const baseUrl = vars.get("ANTHROPIC_BASE_URL");
1506
- return {
1507
- apiKey,
1508
- model,
1509
- ...baseUrl ? { baseUrl } : {}
1510
- };
1511
- }
1512
- /** GOOGLE_API_KEY / GOOGLE_MODEL(可选 GOOGLE_BASE_URL);缺项返回 undefined */
1513
- function loadGoogleConfigFromEnv(dotenvPath = ".env") {
1514
- const vars = readEnvVars(dotenvPath);
1515
- if (!vars) return void 0;
1516
- const apiKey = vars.get("GOOGLE_API_KEY");
1517
- const model = vars.get("GOOGLE_MODEL");
1518
- if (!apiKey || !model) return void 0;
1519
- const baseUrl = vars.get("GOOGLE_BASE_URL");
1520
- return {
1521
- apiKey,
1522
- model,
1523
- ...baseUrl ? { baseUrl } : {}
1524
- };
1525
- }
1526
- let capabilitiesCache;
1527
- /**
1528
- * 探测 LLM 能力(结果模块级缓存):
1529
- * - available:GET /models 可达(与 checkAvailability 同语义)
1530
- * - nonStreaming:最小非流式 chat completion(max_tokens=1,短超时);
1531
- * HTTP 400/404 或明确不支持非流式的错误 → false
1532
- * - streaming 能力由 available 推断(SSE 解析在 SDK 侧实现)
1533
- * 环境变量 VITE_LLM_NON_STREAMING=0 可显式强制 nonStreaming=false(模拟流式-only 回归)。
1534
- */
1535
- function probeLlmCapabilities(config) {
1536
- capabilitiesCache ??= doProbeLlmCapabilities(config);
1537
- return capabilitiesCache;
1538
- }
1539
- async function doProbeLlmCapabilities(config) {
1540
- const baseUrl = config.baseUrl.replace(/\/+$/, "");
1541
- const headers = { authorization: `Bearer ${config.apiKey}` };
1542
- let available = false;
1543
- try {
1544
- available = (await fetch(`${baseUrl}/models`, {
1545
- headers,
1546
- signal: AbortSignal.timeout(1e4)
1547
- })).ok;
1548
- } catch {
1549
- available = false;
1550
- }
1551
- if (!available) return {
1552
- available: false,
1553
- nonStreaming: false,
1554
- reason: "endpoint unreachable or unauthorized"
1555
- };
1556
- if (process.env["VITE_LLM_NON_STREAMING"] === "0") return {
1557
- available: true,
1558
- nonStreaming: false,
1559
- reason: "non-streaming disabled via VITE_LLM_NON_STREAMING=0 (simulated)"
1560
- };
1561
- try {
1562
- const res = await fetch(`${baseUrl}/chat/completions`, {
1563
- method: "POST",
1564
- headers: {
1565
- ...headers,
1566
- "content-type": "application/json"
1567
- },
1568
- body: JSON.stringify({
1569
- model: config.model,
1570
- messages: [{
1571
- role: "user",
1572
- content: "OK"
1573
- }],
1574
- max_tokens: 1
1575
- }),
1576
- signal: AbortSignal.timeout(15e3)
1577
- });
1578
- const contentType = res.headers.get("content-type") ?? "";
1579
- if (res.ok && !contentType.includes("text/event-stream")) return {
1580
- available: true,
1581
- nonStreaming: true
1582
- };
1583
- const detail = (await res.text().catch(() => "")).slice(0, 200);
1584
- return {
1585
- available: true,
1586
- nonStreaming: false,
1587
- reason: `endpoint rejects non-streaming chat completions (HTTP ${res.status})${detail ? `: ${detail}` : ""}`
1588
- };
1589
- } catch (e) {
1590
- return {
1591
- available: true,
1592
- nonStreaming: false,
1593
- reason: `non-streaming probe failed: ${e instanceof Error ? e.message : String(e)}`
1594
- };
1595
- }
1596
- }
1597
1823
 
1598
1824
  //#endregion
1599
- 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 };
1825
+ export { NodeScriptExecutor as a, SandboxedScriptExecutor as c, readArchiveManifest as d, NodeFS as i, SkillManager as l, FileArtifactStore as n, OxcSchemaInferer as o, FileMemoryStore as r, ProcessSandboxExecutor as s, CliUiBridge as t, exportArchive as u };