@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.
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +17 -13
- package/dist/{dist-D7MsoMPx.js → dist-BQzncxXg.js} +70 -3
- package/dist/{dist-CV64gN62.js → dist-BXpDDZpR.js} +255 -116
- package/dist/{dist-CeNAzFYi.js → dist-DNSG9FqC.js} +108 -69
- package/dist/{dist-Chgf2tcy.js → dist-DWFDb1Ww.js} +76 -31
- package/dist/governance.d.ts +98 -94
- package/dist/governance.js +221 -198
- package/dist/{index-DZShzhon.d.ts → index-DJOha4b6.d.ts} +23 -1
- package/dist/{index-DrHelz72.d.ts → index-DRlYzdr2.d.ts} +5 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +6 -2
- package/dist/mcp.js +14 -14
- package/dist/node.d.ts +3 -3
- package/dist/node.js +3 -3
- package/dist/{testing-BN18eqbD.js → testing-BUoXvm1u.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CKm5G_eQ-BqyXnvoR.d.ts → types-CKm5G_eQ-krKWW8WV.d.ts} +32 -2
- package/dist/ui-react.d.ts +3 -1
- package/dist/ui-react.js +8 -1
- package/dist/ui-vue.d.ts +3 -1
- package/dist/ui-vue.js +8 -1
- package/dist/ui.d.ts +20 -6
- package/dist/ui.js +3 -3
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { u as WebSkillError } from "./dist-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
822
|
-
|
|
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
|
-
*
|
|
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")
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1054
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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 {
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(() =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1413
|
-
|
|
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
|
|
1456
|
+
res = await fetchWithSsrfGuard(source, fetchImpl);
|
|
1419
1457
|
} catch (e) {
|
|
1420
|
-
|
|
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),
|