@webskill/sdk 0.2.4 → 0.2.6

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,4 +1,4 @@
1
- import { u as WebSkillError } from "./dist-D7MsoMPx.js";
1
+ import { u as WebSkillError } from "./dist-BQzncxXg.js";
2
2
 
3
3
  //#region ../ui/dist/index.js
4
4
  /** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
@@ -78,8 +78,16 @@ function interactionToFormModel(request) {
78
78
  }
79
79
  const isEmpty = (v) => v === void 0 || v === "";
80
80
  /**
81
+ * 控件名 → 属性选择器:优先 CSS.escape(标识符形式,免引号转义);
82
+ * 无 CSS.escape 的环境退化为引号包裹 + 转义反斜杠/双引号(防选择器注入崩溃)
83
+ */
84
+ function controlSelector(name) {
85
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return `[data-webskill-control=${CSS.escape(name)}]`;
86
+ return `[data-webskill-control="${name.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"]`;
87
+ }
88
+ /**
81
89
  * 严格类型化控件值收集:
82
- * - number 空串 → undefined(非 0)
90
+ * - number 空串 → undefined(非 0);非数值输入(NaN)按缺失处理(required 时入 missingRequired
83
91
  * - select 用 option 值本身经 JSON 编码比对命中(对象值可正确命中)
84
92
  * - required 空值列入 missingRequired(由调用方阻止提交并标记)
85
93
  */
@@ -87,13 +95,17 @@ function collectValues(controls, container) {
87
95
  const values = {};
88
96
  const missingRequired = [];
89
97
  for (const control of controls) {
90
- const el = container.querySelector(`[data-webskill-control="${control.name}"]`);
98
+ const el = container.querySelector(controlSelector(control.name));
91
99
  let value;
92
100
  if (el === null) value = void 0;
93
101
  else if (control.control === "boolean") value = el.checked;
94
102
  else if (control.control === "number") {
95
103
  const raw = el.value.trim();
96
- value = raw === "" ? void 0 : Number(raw);
104
+ if (raw === "") value = void 0;
105
+ else {
106
+ const n = Number(raw);
107
+ value = Number.isNaN(n) ? void 0 : n;
108
+ }
97
109
  } else if (control.control === "select") {
98
110
  const attr = el.value;
99
111
  const hit = control.options?.find((o) => JSON.stringify(o.value) === attr);
@@ -736,12 +748,15 @@ var VercelUiBridge = class {
736
748
  * - 每行一条语句 `identifier = Expression`,`root = Component(...)` 为入口
737
749
  * - 组件调用参数为位置参数;字符串双引号反斜杠转义
738
750
  * - 组件词汇(与应用 Library 约定):Form(title, children)、
751
+ * Text(content)(纯展示文本,authorize 的能力描述用)、
739
752
  * TextField(name, label, required?, defaultValue?)、NumberField、BooleanField(name, label, default?)、
740
753
  * SelectField(name, label, options)、SubmitButton(label, actionType, requestId)、
741
754
  * CancelButton(label, actionType, requestId)
742
755
  */
743
756
  const OPENUI_SUBMIT_ACTION = "webskill:submit";
744
757
  const OPENUI_CANCEL_ACTION = "webskill:cancel";
758
+ /** authorize 的 Allow 专用动作(提交即批准 → value:true;Deny 走 cancel → cancelled:true) */
759
+ const OPENUI_AUTHORIZE_ACTION = "webskill:authorize";
745
760
  const str = (s) => JSON.stringify(s);
746
761
  const val = (v) => JSON.stringify(v ?? null);
747
762
  function fieldStatements(fields) {
@@ -808,18 +823,19 @@ function toOpenUiLang(request) {
808
823
  break;
809
824
  case "authorize":
810
825
  title = "Authorization required";
811
- fields = [{
812
- name: "approved",
813
- label: request.message,
814
- control: "boolean",
815
- defaultValue: false
816
- }];
826
+ fields = [];
817
827
  break;
818
828
  }
819
829
  lines.push(...fieldStatements(fields));
820
830
  const children = fields.map((_, i) => `f${i}`);
821
- lines.push(`submit = SubmitButton("Submit", ${str(OPENUI_SUBMIT_ACTION)}, ${str(request.id)})`);
822
- lines.push(`cancel = CancelButton("Cancel", ${str(OPENUI_CANCEL_ACTION)}, ${str(request.id)})`);
831
+ const isAuthorize = request.type === "authorize";
832
+ if (isAuthorize) {
833
+ lines.push(`message = Text(${str(request.message)})`);
834
+ children.push("message");
835
+ }
836
+ const submitAction = isAuthorize ? OPENUI_AUTHORIZE_ACTION : OPENUI_SUBMIT_ACTION;
837
+ lines.push(`submit = SubmitButton(${str(isAuthorize ? "Allow" : "Submit")}, ${str(submitAction)}, ${str(request.id)})`);
838
+ lines.push(`cancel = CancelButton(${str(isAuthorize ? "Deny" : "Cancel")}, ${str(OPENUI_CANCEL_ACTION)}, ${str(request.id)})`);
823
839
  lines.push(`root = Form(${val(title)}, [${[
824
840
  ...children,
825
841
  "submit",
@@ -838,6 +854,10 @@ function fromOpenUiAction(action) {
838
854
  id,
839
855
  cancelled: true
840
856
  };
857
+ if (a.type === "webskill:authorize") return {
858
+ id,
859
+ value: true
860
+ };
841
861
  if (a.type === "webskill:submit") {
842
862
  const formState = a.formState;
843
863
  if (formState) return {
@@ -1002,20 +1022,70 @@ function toA2uiMessages(request) {
1002
1022
  return messages;
1003
1023
  }
1004
1024
  /**
1005
- * A2UI client→server action 事件 → InteractionResponse
1025
+ * InteractionRequest.type 解码提交值(绑定模型原样回传的是表单对象):
1026
+ * confirm 仅 confirmed===true 批准;select 还原原始 option 值(非字符串值经 JSON 编码比对);
1027
+ * form 的 number 字段转 number(NaN 保留原值);ask 取 answer;authorize 提交即批准。
1028
+ * 纯转换器:任何直接使用 fromA2uiAction 的消费者都应经此拿到正确类型。
1029
+ * @experimental
1030
+ */
1031
+ function decodeInteractionResponse(request, response) {
1032
+ if (response.cancelled) return response;
1033
+ const values = response.value;
1034
+ switch (request.type) {
1035
+ case "confirm": return {
1036
+ ...response,
1037
+ value: values?.confirmed === true
1038
+ };
1039
+ case "ask": return {
1040
+ ...response,
1041
+ value: values?.answer
1042
+ };
1043
+ case "select": {
1044
+ const raw = values?.selected;
1045
+ const option = request.options.find((o) => o.value === raw || JSON.stringify(o.value) === raw);
1046
+ return {
1047
+ ...response,
1048
+ value: option ? option.value : raw
1049
+ };
1050
+ }
1051
+ case "form": {
1052
+ if (typeof values !== "object" || values === null) return response;
1053
+ const out = { ...values };
1054
+ for (const field of request.fields) if (field.type === "number" && out[field.name] !== void 0) {
1055
+ const n = Number(out[field.name]);
1056
+ if (!Number.isNaN(n)) out[field.name] = n;
1057
+ }
1058
+ return {
1059
+ ...response,
1060
+ value: out
1061
+ };
1062
+ }
1063
+ case "authorize": return {
1064
+ ...response,
1065
+ value: true
1066
+ };
1067
+ default: return response;
1068
+ }
1069
+ }
1070
+ /**
1071
+ * A2UI client→server action 事件 → InteractionResponse。
1072
+ * 传入 request 时按类型解码提交值(见 decodeInteractionResponse)。
1006
1073
  * @experimental
1007
1074
  */
1008
- function fromA2uiAction(event) {
1075
+ function fromA2uiAction(event, request) {
1009
1076
  const e = event ?? {};
1010
1077
  const id = String(e.context?.["requestId"] ?? "");
1011
1078
  if (e.name === "webskill:cancel") return {
1012
1079
  id,
1013
1080
  cancelled: true
1014
1081
  };
1015
- if (e.name === "webskill:submit") return {
1016
- id,
1017
- value: e.context?.["values"] ?? e.context?.["form"] ?? e.context
1018
- };
1082
+ if (e.name === "webskill:submit") {
1083
+ const response = {
1084
+ id,
1085
+ value: e.context?.["values"] ?? e.context?.["form"] ?? e.context
1086
+ };
1087
+ return request ? decodeInteractionResponse(request, response) : response;
1088
+ }
1019
1089
  return {
1020
1090
  id,
1021
1091
  value: e.context
@@ -1041,6 +1111,8 @@ function loadA2uiRuntime() {
1041
1111
  var LitRendererBridge = class {
1042
1112
  #mount;
1043
1113
  #doc;
1114
+ /** 进行中的交互:id → resolve 与 surface 元素(cancel 时清理并以 cancelled resolve) */
1115
+ #pending = /* @__PURE__ */ new Map();
1044
1116
  constructor(deps) {
1045
1117
  this.#mount = deps.mount;
1046
1118
  this.#doc = deps.document ?? deps.mount.ownerDocument;
@@ -1050,75 +1122,42 @@ var LitRendererBridge = class {
1050
1122
  const messages = toA2uiMessages(input);
1051
1123
  const surfaceId = `webskill-${input.id}`;
1052
1124
  return new Promise((resolve) => {
1053
- const cleanup = (el) => el?.remove();
1054
- let surfaceEl;
1125
+ const entry = { resolve };
1126
+ this.#pending.set(input.id, entry);
1055
1127
  const processor = new MessageProcessor([basicCatalog], (action) => {
1056
1128
  const surfaces = processor.getClientDataModel("v0.9.1")?.surfaces;
1057
1129
  const formValues = typeof surfaces?.[surfaceId] === "object" && surfaces[surfaceId] !== null ? surfaces[surfaceId]["form"] ?? action.context : action.context;
1058
- cleanup(surfaceEl);
1059
- const response = fromA2uiAction({
1130
+ this.#pending.delete(input.id);
1131
+ entry.surfaceEl?.remove();
1132
+ resolve(fromA2uiAction({
1060
1133
  ...action,
1061
1134
  context: {
1062
1135
  ...action.context,
1063
1136
  values: formValues
1064
1137
  }
1065
- });
1066
- resolve(this.#decodeValue(input, response));
1138
+ }, input));
1067
1139
  }, { version: "v0.9.1" });
1068
1140
  processor.onSurfaceCreated((surface) => {
1069
1141
  const el = this.#doc.createElement("a2ui-surface");
1070
1142
  el.surface = surface;
1071
- surfaceEl = el;
1143
+ entry.surfaceEl = el;
1072
1144
  this.#mount.appendChild(el);
1073
1145
  });
1074
1146
  processor.processMessages(messages);
1075
1147
  });
1076
1148
  }
1077
- /**
1078
- * 按 InteractionRequest.type 解码提交值(绑定模型原样回传的是表单对象):
1079
- * confirm confirmed===true 批准;select 还原原始 option 值(非字符串值经 JSON 编码比对);
1080
- * form 的 number 字段转 number;ask 取 answer;authorize 提交即批准。
1081
- */
1082
- #decodeValue(input, response) {
1083
- if (response.cancelled) return response;
1084
- const values = response.value;
1085
- switch (input.type) {
1086
- case "confirm": return {
1087
- ...response,
1088
- value: values?.confirmed === true
1089
- };
1090
- case "ask": return {
1091
- ...response,
1092
- value: values?.answer
1093
- };
1094
- case "select": {
1095
- const raw = values?.selected;
1096
- const option = input.options.find((o) => o.value === raw || JSON.stringify(o.value) === raw);
1097
- return {
1098
- ...response,
1099
- value: option ? option.value : raw
1100
- };
1101
- }
1102
- case "form": {
1103
- if (typeof values !== "object" || values === null) return response;
1104
- const out = { ...values };
1105
- for (const field of input.fields) if (field.type === "number" && out[field.name] !== void 0) {
1106
- const n = Number(out[field.name]);
1107
- if (!Number.isNaN(n)) out[field.name] = n;
1108
- }
1109
- return {
1110
- ...response,
1111
- value: out
1112
- };
1113
- }
1114
- case "authorize": return {
1115
- ...response,
1116
- value: true
1117
- };
1118
- default: return response;
1119
- }
1149
+ /** runtime 交互超时/取消:移除 surface 并以 cancelled resolve pending Promise(防悬挂 + DOM 残留) */
1150
+ cancel(id) {
1151
+ const entry = this.#pending.get(id);
1152
+ if (!entry) return;
1153
+ this.#pending.delete(id);
1154
+ entry.surfaceEl?.remove();
1155
+ entry.resolve({
1156
+ id,
1157
+ cancelled: true
1158
+ });
1120
1159
  }
1121
1160
  };
1122
1161
 
1123
1162
  //#endregion
1124
- export { renderRenderResult as C, toVercelToolInvocation as D, toOpenUiLang as E, renderMiniMarkdown as S, toA2uiMessages as T, fromOpenUiAction as _, CHART_PALETTE as a, renderBlocks as b, OPENUI_SUBMIT_ACTION as c, WEBSKILL_STYLES_CSS as d, WebFormBridge as f, fromA2uiAction as g, ensureStyles as h, A2UI_VERSION as i, VERCEL_INTERACTION_TOOL_NAME as l, collectValues as m, A2UI_CANCEL_ACTION as n, LitRendererBridge as o, chartToTable as p, A2UI_SUBMIT_ACTION as r, OPENUI_CANCEL_ACTION as s, A2UI_BASIC_CATALOG_ID as t, VercelUiBridge as u, fromVercelToolResult as v, shapeInteractionValue as w, renderMiniChart as x, interactionToFormModel as y };
1163
+ export { renderMiniChart as C, toA2uiMessages as D, shapeInteractionValue as E, toOpenUiLang as O, renderBlocks as S, renderRenderResult as T, ensureStyles as _, CHART_PALETTE as a, fromVercelToolResult as b, OPENUI_CANCEL_ACTION as c, VercelUiBridge as d, WEBSKILL_STYLES_CSS as f, decodeInteractionResponse as g, collectValues as h, A2UI_VERSION as i, toVercelToolInvocation as k, OPENUI_SUBMIT_ACTION as l, chartToTable as m, A2UI_CANCEL_ACTION as n, LitRendererBridge as o, WebFormBridge as p, A2UI_SUBMIT_ACTION as r, OPENUI_AUTHORIZE_ACTION as s, A2UI_BASIC_CATALOG_ID as t, VERCEL_INTERACTION_TOOL_NAME as u, fromA2uiAction as v, renderMiniMarkdown as w, interactionToFormModel as x, fromOpenUiAction as y };
@@ -1,5 +1,5 @@
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-D7MsoMPx.js";
2
- import { A as isNetworkAllowed, C as bridgeError, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-CV64gN62.js";
1
+ import { A as resolveArchiveLimits, C as messageOf, D as readResponseWithLimit, E as parseSkillPackManifest, M as unzipWithLimits, N as validateSkills, P as verifyManifest, T as parseSkillMarkdown, b as exportSkills, d as assertRemoteUrlAllowed, h as buildManifest, i as SKILL_MANIFEST_FILE, j as resolveInsideRoot, p as atomicWriteText, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, x as isValidSkillName } from "./dist-BQzncxXg.js";
2
+ import { A as isNetworkAllowed, C as bridgeError, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-BXpDDZpR.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { unzipSync, zipSync } from "fflate";
5
5
  import { existsSync, promises, realpathSync } from "node:fs";
@@ -295,7 +295,6 @@ var NodeScriptExecutor = class {
295
295
  };
296
296
  const baseName$1 = (p) => p.split("/").pop() ?? p;
297
297
  const toPlatform$3 = (p) => p.split("/").join(path.sep);
298
- const messageOf$6 = (e) => e instanceof Error ? e.message : String(e);
299
298
  /**
300
299
  * 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
301
300
  * runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
@@ -381,7 +380,7 @@ var SandboxedScriptExecutor = class {
381
380
  content: [],
382
381
  error: {
383
382
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
384
- message: messageOf$6(e)
383
+ message: messageOf(e)
385
384
  }
386
385
  };
387
386
  }
@@ -422,7 +421,7 @@ var SandboxedScriptExecutor = class {
422
421
  content: [],
423
422
  error: {
424
423
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
425
- message: messageOf$6(e)
424
+ message: messageOf(e)
426
425
  }
427
426
  };
428
427
  }
@@ -473,7 +472,7 @@ var SandboxedScriptExecutor = class {
473
472
  respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
474
473
  return;
475
474
  }
476
- onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$6(e))));
475
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
477
476
  return;
478
477
  }
479
478
  if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
@@ -534,13 +533,12 @@ var SandboxedScriptExecutor = class {
534
533
  }
535
534
  }
536
535
  } catch (e) {
537
- return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf$6(e));
536
+ return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
538
537
  }
539
538
  }
540
539
  };
541
540
  const baseName = (p) => p.split("/").pop() ?? p;
542
541
  const toPlatform$2 = (p) => p.split("/").join(path.sep);
543
- const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
544
542
  const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
545
543
  const readAllow = (p) => [`--allow-fs-read=${p}`, `--allow-fs-read=${realpathSync(p)}`];
546
544
  const writeAllow = (p) => [`--allow-fs-write=${p}`, `--allow-fs-write=${realpathSync(p)}`];
@@ -572,6 +570,7 @@ var ProcessSandboxExecutor = class {
572
570
  #approval;
573
571
  #slots = [];
574
572
  #waiters = [];
573
+ #disposed = false;
575
574
  constructor(fs, options = {}) {
576
575
  this.#fs = fs;
577
576
  this.#options = options;
@@ -589,8 +588,9 @@ var ProcessSandboxExecutor = class {
589
588
  get poolSize() {
590
589
  return this.#options.poolSize ?? 2;
591
590
  }
592
- /** 池全部子进程销毁(测试收尾/进程退出前调用) */
591
+ /** 池全部子进程销毁(测试收尾/进程退出前调用);排队中的 acquire 一律 reject(不悬挂) */
593
592
  async dispose() {
593
+ this.#disposed = true;
594
594
  for (const slot of this.#slots) {
595
595
  slot.child.kill();
596
596
  await rm(slot.artifactDir, {
@@ -599,7 +599,8 @@ var ProcessSandboxExecutor = class {
599
599
  }).catch(() => void 0);
600
600
  }
601
601
  this.#slots = [];
602
- this.#waiters = [];
602
+ const waiters = this.#waiters.splice(0);
603
+ for (const waiter of waiters) waiter.reject(new WebSkillError("TOOL_EXECUTION_FAILED", "Process sandbox executor disposed while a caller was waiting for a pool slot"));
603
604
  }
604
605
  async #locateScript(skillRoot, scriptName) {
605
606
  const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
@@ -612,6 +613,7 @@ var ProcessSandboxExecutor = class {
612
613
  /** 取一个可用子进程(同 key 复用 / 淘汰 idle 重生 / 扩容 / 排队) */
613
614
  async #acquire(key) {
614
615
  for (;;) {
616
+ if (this.#disposed) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Process sandbox executor is disposed");
615
617
  const idle = this.#slots.filter((s) => !s.busy);
616
618
  const match = idle.find((s) => s.key === key);
617
619
  if (match) {
@@ -633,7 +635,10 @@ var ProcessSandboxExecutor = class {
633
635
  this.#slots.push(slot);
634
636
  return slot;
635
637
  }
636
- await new Promise((resolve) => this.#waiters.push(resolve));
638
+ await new Promise((resolve, reject) => this.#waiters.push({
639
+ resolve,
640
+ reject
641
+ }));
637
642
  }
638
643
  }
639
644
  /** 执行后回收:kill 旧子进程,补位重生同 key 新子进程(温池保持),唤醒排队 */
@@ -645,13 +650,27 @@ var ProcessSandboxExecutor = class {
645
650
  recursive: true,
646
651
  force: true
647
652
  }).catch(() => void 0);
648
- this.#spawnSlot(slot.key).then((fresh) => {
653
+ this.#spawnSlot(slot.key).then(async (fresh) => {
654
+ if (this.#disposed) {
655
+ fresh.child.kill();
656
+ await rm(fresh.artifactDir, {
657
+ recursive: true,
658
+ force: true
659
+ }).catch(() => void 0);
660
+ return;
661
+ }
649
662
  this.#slots.push(fresh);
650
- }).catch(() => void 0).finally(() => {
663
+ }).catch((e) => {
664
+ this.#warn(`Failed to respawn sandbox child for pool maintenance: ${messageOf(e)}`);
665
+ }).finally(() => {
651
666
  const waiter = this.#waiters.shift();
652
- if (waiter) waiter();
667
+ if (waiter) waiter.resolve();
653
668
  });
654
669
  }
670
+ #warn(message) {
671
+ if (this.#options.onWarning) this.#options.onWarning(message);
672
+ else console.warn(message);
673
+ }
655
674
  async #spawnSlot(key) {
656
675
  const artifactDir = await mkdtemp(path.join(tmpdir(), "webskill-psbx-out-")).then((d) => d.split(path.sep).join("/"));
657
676
  const entry = processEntryPath();
@@ -712,7 +731,7 @@ var ProcessSandboxExecutor = class {
712
731
  content: [],
713
732
  error: {
714
733
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
715
- message: messageOf$5(e)
734
+ message: messageOf(e)
716
735
  }
717
736
  };
718
737
  }
@@ -755,7 +774,7 @@ var ProcessSandboxExecutor = class {
755
774
  content: [],
756
775
  error: {
757
776
  code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
758
- message: messageOf$5(e)
777
+ message: messageOf(e)
759
778
  }
760
779
  };
761
780
  } finally {
@@ -794,7 +813,7 @@ var ProcessSandboxExecutor = class {
794
813
  respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
795
814
  return;
796
815
  }
797
- onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf$5(e))));
816
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
798
817
  return;
799
818
  }
800
819
  if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
@@ -863,7 +882,7 @@ var ProcessSandboxExecutor = class {
863
882
  }
864
883
  }
865
884
  } catch (e) {
866
- return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf$5(e));
885
+ return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
867
886
  }
868
887
  }
869
888
  };
@@ -1195,7 +1214,6 @@ var CliUiBridge = class {
1195
1214
  return new Promise((resolve) => this.#waiters.push(resolve));
1196
1215
  }
1197
1216
  };
1198
- const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
1199
1217
  const toPlatform$1 = (p) => p.split("/").join(path.sep);
1200
1218
  const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
1201
1219
  let tarModule$1;
@@ -1244,7 +1262,7 @@ async function extractTarFile(archivePath, destRoot, limits) {
1244
1262
  });
1245
1263
  } catch (e) {
1246
1264
  if (e instanceof WebSkillError) throw e;
1247
- throw new WebSkillError("INSTALL_FAILED", `Failed to extract tar archive: ${messageOf$4(e)}`, e);
1265
+ throw new WebSkillError("INSTALL_FAILED", `Failed to extract tar archive: ${messageOf(e)}`, e);
1248
1266
  }
1249
1267
  }
1250
1268
  /** 递归列出目录内全部文件(相对路径,posix 分隔,不含目录条目) */
@@ -1288,7 +1306,6 @@ async function removeDirQuiet(fs, dir) {
1288
1306
  if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
1289
1307
  } catch {}
1290
1308
  }
1291
- const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
1292
1309
  let tarModule;
1293
1310
  async function loadTar() {
1294
1311
  tarModule ??= await import("tar").catch((e) => {
@@ -1310,7 +1327,7 @@ async function exportArchive(fs, skillRoot, options) {
1310
1327
  return options.outPath;
1311
1328
  } catch (e) {
1312
1329
  if (e instanceof WebSkillError) throw e;
1313
- throw new WebSkillError("EXPORT_FAILED", `Failed to export ${options.format} archive: ${messageOf$3(e)}`, e);
1330
+ throw new WebSkillError("EXPORT_FAILED", `Failed to export ${options.format} archive: ${messageOf(e)}`, e);
1314
1331
  }
1315
1332
  }
1316
1333
  /** 只解出归档中的 webskill.skill-manifest.json 条目(安装前预览) */
@@ -1338,13 +1355,12 @@ async function readArchiveManifest(fs, archivePath) {
1338
1355
  }
1339
1356
  });
1340
1357
  } catch (e) {
1341
- throw new WebSkillError("EXPORT_FAILED", `Failed to read tar archive: ${messageOf$3(e)}`, e);
1358
+ throw new WebSkillError("EXPORT_FAILED", `Failed to read tar archive: ${messageOf(e)}`, e);
1342
1359
  }
1343
1360
  if (manifestText === void 0) throw notFound();
1344
1361
  return JSON.parse(manifestText);
1345
1362
  }
1346
1363
  const _execFileP = promisify(execFile);
1347
- const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
1348
1364
  /**
1349
1365
  * 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
1350
1366
  * git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
@@ -1360,7 +1376,7 @@ async function execFileP(command, args, options = {}) {
1360
1376
  }
1361
1377
  /** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
1362
1378
  function commandFailed(command, e) {
1363
- return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf$2(e)}`, e);
1379
+ return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf(e)}`, e);
1364
1380
  }
1365
1381
  /** git url 协议白名单:https:// 与 git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
1366
1382
  * 其余显式协议(ext::/file:// 等)一律拒绝 */
@@ -1409,15 +1425,38 @@ async function stageGit(source, ctx) {
1409
1425
  function sha256Hex(data) {
1410
1426
  return createHash("sha256").update(data).digest("hex");
1411
1427
  }
1412
- const messageOf$1 = (e) => e instanceof Error ? e.message : String(e);
1413
- /** http 源:下载归档(Content-Length + 流式累计上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
1428
+ /** 重定向跳数上限(SSRF 防护:逐跳重新校验目标 URL) */
1429
+ const MAX_REDIRECT_HOPS = 3;
1430
+ /**
1431
+ * 带 SSRF 防护的下载:手动跟随重定向(默认 ≤3 跳),初始 URL 与每一跳目标
1432
+ * 都过 assertRemoteUrlAllowed(https 默认;私有/环回/链路本地默认拒绝)。
1433
+ */
1434
+ async function fetchWithSsrfGuard(source, fetchImpl) {
1435
+ const policy = {
1436
+ allowHttp: source.allowHttp ?? false,
1437
+ allowPrivateHosts: source.allowPrivateHosts ?? false
1438
+ };
1439
+ let url = assertRemoteUrlAllowed(source.url, policy);
1440
+ for (let hop = 0;; hop++) {
1441
+ const res = await fetchImpl(url.href, { redirect: "manual" });
1442
+ const location = res.headers.get("location");
1443
+ if (res.status >= 300 && res.status < 400 && location) {
1444
+ if (hop >= MAX_REDIRECT_HOPS) throw new WebSkillError("INSTALL_FAILED", `Download exceeded the redirect limit of ${MAX_REDIRECT_HOPS} hops`);
1445
+ url = assertRemoteUrlAllowed(new URL(location, url).href, policy);
1446
+ continue;
1447
+ }
1448
+ return res;
1449
+ }
1450
+ }
1451
+ /** http 源:下载归档(SSRF 防护 + Content-Length/流式上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
1414
1452
  async function stageHttp(source, ctx, expectedSha256) {
1415
1453
  const fetchImpl = ctx.fetchImpl ?? fetch;
1416
1454
  let res;
1417
1455
  try {
1418
- res = await fetchImpl(source.url);
1456
+ res = await fetchWithSsrfGuard(source, fetchImpl);
1419
1457
  } catch (e) {
1420
- throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
1458
+ if (e instanceof WebSkillError) throw e;
1459
+ throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf(e)}`, e);
1421
1460
  }
1422
1461
  if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
1423
1462
  const data = await readResponseWithLimit(res, ctx.archiveLimits);
@@ -1466,13 +1505,19 @@ async function stageLocal(source, ctx) {
1466
1505
  /** npm 包名(@scope/name 或 name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
1467
1506
  const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
1468
1507
  const VERSION_RE = /^[a-z0-9._~^>=<x*+-]+$/i;
1508
+ /**
1509
+ * 本地路径字符白名单(Windows 下 npm 经 cmd /c 代理二次解析,
1510
+ * `& | < > ^ " ' % ! ;` 与空格一律拒绝;允许 Windows 盘符 `C:\` 与正/反斜杠)
1511
+ */
1512
+ const LOCAL_PATH_RE = /^[A-Za-z0-9._~+@/\\:-]+$/;
1469
1513
  function assertNpmSpecSafe(source) {
1470
1514
  const name = source.packageName;
1471
1515
  const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
1472
1516
  if (name.startsWith("--") || !PACKAGE_NAME_RE.test(name) && !isLocalPath) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package name: ${JSON.stringify(name)}`);
1517
+ if (isLocalPath && !LOCAL_PATH_RE.test(name)) throw new WebSkillError("INSTALL_FAILED", `Local npm path contains characters outside the safe whitelist: ${JSON.stringify(name)}`);
1473
1518
  if (source.version !== void 0 && (!VERSION_RE.test(source.version) || source.version.startsWith("--"))) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package version: ${JSON.stringify(source.version)}`);
1474
1519
  }
1475
- /** npm 源:npm pack → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
1520
+ /** npm 源:npm pack --ignore-scripts → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
1476
1521
  async function stageNpm(source, ctx) {
1477
1522
  assertNpmSpecSafe(source);
1478
1523
  const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
@@ -1480,6 +1525,7 @@ async function stageNpm(source, ctx) {
1480
1525
  try {
1481
1526
  ({stdout} = await execFileP("npm", [
1482
1527
  "pack",
1528
+ "--ignore-scripts",
1483
1529
  "--pack-destination",
1484
1530
  ctx.stagingRoot,
1485
1531
  "--",
@@ -1596,7 +1642,6 @@ async function verifyIntegrity(fs, skillRoot) {
1596
1642
  for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
1597
1643
  return verifyManifest(manifest, actualHashes, actualFiles);
1598
1644
  }
1599
- const messageOf = (e) => e instanceof Error ? e.message : String(e);
1600
1645
  const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
1601
1646
  /**
1602
1647
  * 技能管理门面:统一安装管线(staging → 解析 name → 校验 → 拷贝 → manifest → lockfile),