@webskill/sdk 0.0.4 → 0.0.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 +47 -5
- package/dist/browser.js +225 -18
- package/dist/{dist-Bvhbxrrt.js → dist-33_zuOCm.js} +141 -13
- package/dist/{dist-RcqvzAGF.js → dist-BQruQ9Hv.js} +243 -9
- package/dist/{dist-COsE72Ct.js → dist-nXiR40hi.js} +715 -331
- package/dist/governance.d.ts +4 -3
- package/dist/governance.js +13 -4
- package/dist/{index-CqDIreSE.d.ts → index-DCifjtJx.d.ts} +176 -8
- package/dist/{index-BsbfEiaC.d.ts → index-vi7GPemR.d.ts} +13 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/ui-react.d.ts +2 -2
- package/dist/ui-react.js +13 -3
- package/dist/ui-vue.d.ts +2 -2
- package/dist/ui-vue.js +10 -3
- package/dist/ui.d.ts +17 -5
- package/dist/ui.js +3 -3
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as InteractionPolicy, B as LlmResponse, Et as UiBridge, Ht as parseBridgeRequest, L as LlmClient, M as InteractionResponse, Ot as WebSkillApi, V as LlmStreamEvent, Xt as FileSystemProvider, Yt as FileStat, Z as NetworkPolicy, _t as ScriptExecutor, b as ExternalToolSource, c as ApprovalScope, cn as SkillInstallSource, d as BridgeCapabilities, dn as SkillManifest, gn as SkillsLockfile, gt as ScriptExecutionContext, j as InteractionRequest, jt as bridgeError, m as BridgeResponse, ot as RenderResultRequest, p as BridgeRequest, vn as VerifyResult, xt as ToolResult, y as ExternalSkillProvider, yt as ToolDefinition } from "./index-DCifjtJx.js";
|
|
2
2
|
//#region ../browser/dist/index.d.ts
|
|
3
3
|
//#region src/fs/featureDetection.d.ts
|
|
4
4
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -28,17 +28,22 @@ declare class OpfsProvider implements FileSystemProvider {
|
|
|
28
28
|
//#region src/executor/workerBootstrap.d.ts
|
|
29
29
|
/**
|
|
30
30
|
* Worker 引导源码(字符串 → Blob URL → new Worker(url, {type:'module'}))。
|
|
31
|
-
* 注意:内容为纯 JS,不能含反引号与 ${
|
|
31
|
+
* 注意:内容为纯 JS,不能含反引号与 ${ 占位(外层用模板字符串内嵌);
|
|
32
|
+
* isNetworkAllowed/networkUrlHost 经函数源码注入(匹配逻辑单一来源在
|
|
33
|
+
* runtime/sandbox/networkPolicy.ts,此处禁止复制实现)。
|
|
32
34
|
*
|
|
33
35
|
* 协议(宿主 ↔ Worker):
|
|
34
|
-
* - 宿主 → Worker:{type:'load', id, source} 请求加载脚本定义
|
|
35
|
-
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args} 请求执行
|
|
36
|
+
* - 宿主 → Worker:{type:'load', id, source, networkPolicy?} 请求加载脚本定义
|
|
37
|
+
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args, networkPolicy?} 请求执行
|
|
36
38
|
* - Worker → 宿主:{type:'load-result', id, ok, definition?, error?}
|
|
37
39
|
* - Worker → 宿主:{type:'execute-result', id, ok, value?, stdout, stderr, error?}
|
|
38
40
|
* - Worker → 宿主:{type:'bridge', request} 能力桥 RPC(readReference/writeArtifact/confirm)
|
|
39
41
|
* - 宿主 → Worker:{type:'bridge-response', response}
|
|
42
|
+
* - Worker → 宿主:{type:'network-blocked', host} 网络阻断通知(宿主记 run.warning,URL 脱敏只记 host)
|
|
40
43
|
*
|
|
41
44
|
* .js 源码经 data: URL 动态 import;主线程从不 import 技能脚本。
|
|
45
|
+
* 网络管控:包装 fetch / XMLHttpRequest / WebSocket / EventSource,
|
|
46
|
+
* 策略默认 deny-all,由 load/execute 消息携带更新。
|
|
42
47
|
*/
|
|
43
48
|
declare const WORKER_BOOTSTRAP_SOURCE: string;
|
|
44
49
|
//#endregion
|
|
@@ -80,6 +85,11 @@ declare class BrowserSkillManager {
|
|
|
80
85
|
install(source: SkillInstallSource, options?: {
|
|
81
86
|
expectedSha256?: string;
|
|
82
87
|
}): Promise<SkillManifest>;
|
|
88
|
+
/**
|
|
89
|
+
* 浏览器导出(单技能与包集同构:始终产出含 webskill.skill-pack.json 的 zip 字节,
|
|
90
|
+
* 统一安装路径;保存/下载由应用层处理)。
|
|
91
|
+
*/
|
|
92
|
+
exportArchive(names: string | string[]): Promise<Uint8Array>;
|
|
83
93
|
uninstall(name: string): Promise<void>;
|
|
84
94
|
verifyIntegrity(name: string): Promise<VerifyResult>;
|
|
85
95
|
listInstalled(): Promise<SkillsLockfile>;
|
|
@@ -116,6 +126,12 @@ declare class BrowserWorkerScriptExecutor implements ScriptExecutor {
|
|
|
116
126
|
fs: FileSystemProvider;
|
|
117
127
|
workerFactory?: WorkerFactory;
|
|
118
128
|
capabilities?: BridgeCapabilities;
|
|
129
|
+
/** 网络策略:默认 'deny-all'(白名单见 runtime/sandbox/networkPolicy) */
|
|
130
|
+
networkPolicy?: NetworkPolicy;
|
|
131
|
+
/** require-approval 模式的授权询问出口(缺失时 require-approval 一律拒绝) */
|
|
132
|
+
uiBridge?: UiBridge;
|
|
133
|
+
/** 授权粒度:默认 'once-per-run' */
|
|
134
|
+
approvalScope?: ApprovalScope;
|
|
119
135
|
/** D4 opt-in:配置 esbuild-wasm 资产 URL 后 .ts 经宿主侧转译执行(未配置 .ts → TOOL_UNSUPPORTED) */
|
|
120
136
|
typescript?: TypeScriptSupportOptions;
|
|
121
137
|
});
|
|
@@ -129,6 +145,32 @@ declare class BrowserWorkerScriptExecutor implements ScriptExecutor {
|
|
|
129
145
|
}): Promise<ToolResult>;
|
|
130
146
|
}
|
|
131
147
|
//#endregion
|
|
148
|
+
//#region src/navigator.d.ts
|
|
149
|
+
declare global {
|
|
150
|
+
/** 0.0.6:installWebSkillNavigator 显式装配后可用(不自动挂全局) */
|
|
151
|
+
interface Navigator {
|
|
152
|
+
webskill?: WebSkillApi;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* navigator.webskill 显式装配:OPFS 默认存储 + BrowserWorkerScriptExecutor +
|
|
157
|
+
* BrowserSkillManager → createWebSkillApi;target 提供时挂 target.navigator
|
|
158
|
+
* (缺失则补空对象),否则挂真实 navigator.webskill。
|
|
159
|
+
* target 可注入(测试);返回值即门面本体(挂载只是副作用)。
|
|
160
|
+
*/
|
|
161
|
+
declare function installWebSkillNavigator(config: {
|
|
162
|
+
roots?: string[];
|
|
163
|
+
llm: LlmClient;
|
|
164
|
+
typescript?: TypeScriptSupportOptions;
|
|
165
|
+
/** 测试注入:替换 OPFS 默认 fs(jsdom 无 OPFS) */
|
|
166
|
+
fs?: FileSystemProvider;
|
|
167
|
+
/** 安装目标根(默认与 roots[0] 一致) */
|
|
168
|
+
managedRoot?: string;
|
|
169
|
+
target?: {
|
|
170
|
+
navigator?: Record<string, unknown>;
|
|
171
|
+
};
|
|
172
|
+
}): WebSkillApi;
|
|
173
|
+
//#endregion
|
|
132
174
|
//#region src/host/protocol.d.ts
|
|
133
175
|
/** 主线程 → Worker 的 LLM 配置;mockResponses 供 E2E/测试注入确定性 Mock */
|
|
134
176
|
interface LlmClientConfig {
|
|
@@ -292,4 +334,4 @@ declare class WorkerUiBridge implements UiBridge {
|
|
|
292
334
|
onTextDelta(runId: string, delta: string): Promise<void>;
|
|
293
335
|
}
|
|
294
336
|
//#endregion
|
|
295
|
-
export { type BridgeCapabilities, type BridgeRequest, type BridgeResponse, BrowserSkillManager, BrowserWorkerScriptExecutor, type LlmClientConfig, type MainToWorkerMessage, OpfsProvider, type SerializedError, TsTranspiler, type TypeScriptSupportOptions, WORKER_BOOTSTRAP_SOURCE, type WorkerEvent, type WorkerFactory, type WorkerLike, type WorkerRequest, WorkerRuntimeClient, type WorkerRuntimeClientDeps, type WorkerRuntimeHostOverrides, type WorkerScopeLike, WorkerUiBridge, bridgeError, extractZipWeb, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
|
|
337
|
+
export { type BridgeCapabilities, type BridgeRequest, type BridgeResponse, BrowserSkillManager, BrowserWorkerScriptExecutor, type LlmClientConfig, type MainToWorkerMessage, OpfsProvider, type SerializedError, TsTranspiler, type TypeScriptSupportOptions, WORKER_BOOTSTRAP_SOURCE, type WorkerEvent, type WorkerFactory, type WorkerLike, type WorkerRequest, WorkerRuntimeClient, type WorkerRuntimeClientDeps, type WorkerRuntimeHostOverrides, type WorkerScopeLike, WorkerUiBridge, bridgeError, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
|
package/dist/browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as createWebSkillApi, F as parseBridgeRequest, G as SKILL_PACK_FILE, H as SKILL_MANIFEST_FILE, J as WebSkillError, K as SkillDiscovery, M as mergeCatalogEntries, N as networkUrlHost, P as normalizeToolContent, V as SKILLS_LOCKFILE, X as buildManifest, Y as buildCatalog, _ as ProgressiveRouter, a as CapabilityApproval, at as parseSkillMarkdown, c as FsMemoryStore, dt as verifyManifest, g as OpenAiCompatibleClient, i as AgentLoop, j as isNetworkAllowed, l as FsRunSnapshotStore, lt as resolveInsideRoot, m as MockLlmClient, nt as isValidSkillName, ot as parseSkillPackManifest, s as FsArtifactStore, tt as exportSkills, ut as validateSkills, w as bridgeError, x as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-nXiR40hi.js";
|
|
2
2
|
import { unzipSync } from "fflate";
|
|
3
3
|
|
|
4
4
|
//#region ../browser/dist/index.js
|
|
@@ -134,23 +134,92 @@ var OpfsProvider = class {
|
|
|
134
134
|
};
|
|
135
135
|
/**
|
|
136
136
|
* Worker 引导源码(字符串 → Blob URL → new Worker(url, {type:'module'}))。
|
|
137
|
-
* 注意:内容为纯 JS,不能含反引号与 ${
|
|
137
|
+
* 注意:内容为纯 JS,不能含反引号与 ${ 占位(外层用模板字符串内嵌);
|
|
138
|
+
* isNetworkAllowed/networkUrlHost 经函数源码注入(匹配逻辑单一来源在
|
|
139
|
+
* runtime/sandbox/networkPolicy.ts,此处禁止复制实现)。
|
|
138
140
|
*
|
|
139
141
|
* 协议(宿主 ↔ Worker):
|
|
140
|
-
* - 宿主 → Worker:{type:'load', id, source} 请求加载脚本定义
|
|
141
|
-
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args} 请求执行
|
|
142
|
+
* - 宿主 → Worker:{type:'load', id, source, networkPolicy?} 请求加载脚本定义
|
|
143
|
+
* - 宿主 → Worker:{type:'execute', id, skillName, runId, source, args, networkPolicy?} 请求执行
|
|
142
144
|
* - Worker → 宿主:{type:'load-result', id, ok, definition?, error?}
|
|
143
145
|
* - Worker → 宿主:{type:'execute-result', id, ok, value?, stdout, stderr, error?}
|
|
144
146
|
* - Worker → 宿主:{type:'bridge', request} 能力桥 RPC(readReference/writeArtifact/confirm)
|
|
145
147
|
* - 宿主 → Worker:{type:'bridge-response', response}
|
|
148
|
+
* - Worker → 宿主:{type:'network-blocked', host} 网络阻断通知(宿主记 run.warning,URL 脱敏只记 host)
|
|
146
149
|
*
|
|
147
150
|
* .js 源码经 data: URL 动态 import;主线程从不 import 技能脚本。
|
|
151
|
+
* 网络管控:包装 fetch / XMLHttpRequest / WebSocket / EventSource,
|
|
152
|
+
* 策略默认 deny-all,由 load/execute 消息携带更新。
|
|
148
153
|
*/
|
|
149
154
|
const WORKER_BOOTSTRAP_SOURCE = String.raw`
|
|
150
155
|
'use strict';
|
|
151
156
|
|
|
152
157
|
var pending = new Map();
|
|
153
158
|
var bridgeSeq = 0;
|
|
159
|
+
var networkPolicy = 'deny-all';
|
|
160
|
+
|
|
161
|
+
${isNetworkAllowed.toString()}
|
|
162
|
+
|
|
163
|
+
${networkUrlHost.toString()}
|
|
164
|
+
|
|
165
|
+
function resolveUrl(raw) {
|
|
166
|
+
try {
|
|
167
|
+
return new URL(raw, self.location.href).href;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return String(raw);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function networkGate(rawUrl) {
|
|
174
|
+
var url = resolveUrl(rawUrl);
|
|
175
|
+
if (isNetworkAllowed(networkPolicy, url)) return null;
|
|
176
|
+
var host = networkUrlHost(url);
|
|
177
|
+
self.postMessage({ type: 'network-blocked', host: host });
|
|
178
|
+
var err = new Error('Network request blocked by sandbox network policy: ' + host);
|
|
179
|
+
err.code = 'NETWORK_BLOCKED';
|
|
180
|
+
return err;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (typeof self.fetch === 'function') {
|
|
184
|
+
var originalFetch = self.fetch;
|
|
185
|
+
self.fetch = function (input, init) {
|
|
186
|
+
var raw = typeof input === 'string' ? input : input && input.url ? input.url : String(input);
|
|
187
|
+
var blocked = networkGate(raw);
|
|
188
|
+
if (blocked) return Promise.reject(blocked);
|
|
189
|
+
return originalFetch.call(this, input, init);
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
194
|
+
var originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
195
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
196
|
+
var blocked = networkGate(url);
|
|
197
|
+
if (blocked) throw blocked;
|
|
198
|
+
return originalXhrOpen.apply(this, arguments);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (typeof WebSocket !== 'undefined') {
|
|
203
|
+
var OriginalWebSocket = WebSocket;
|
|
204
|
+
self.WebSocket = function (url, protocols) {
|
|
205
|
+
var blocked = networkGate(url);
|
|
206
|
+
if (blocked) throw blocked;
|
|
207
|
+
return protocols === undefined
|
|
208
|
+
? new OriginalWebSocket(url)
|
|
209
|
+
: new OriginalWebSocket(url, protocols);
|
|
210
|
+
};
|
|
211
|
+
self.WebSocket.prototype = OriginalWebSocket.prototype;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (typeof EventSource !== 'undefined') {
|
|
215
|
+
var OriginalEventSource = EventSource;
|
|
216
|
+
self.EventSource = function (url, config) {
|
|
217
|
+
var blocked = networkGate(url);
|
|
218
|
+
if (blocked) throw blocked;
|
|
219
|
+
return config === undefined ? new OriginalEventSource(url) : new OriginalEventSource(url, config);
|
|
220
|
+
};
|
|
221
|
+
self.EventSource.prototype = OriginalEventSource.prototype;
|
|
222
|
+
}
|
|
154
223
|
|
|
155
224
|
function loadModule(source) {
|
|
156
225
|
var url = 'data:text/javascript;charset=utf-8,' + encodeURIComponent(source);
|
|
@@ -212,6 +281,7 @@ self.onmessage = async function (event) {
|
|
|
212
281
|
}
|
|
213
282
|
|
|
214
283
|
if (msg.type === 'load') {
|
|
284
|
+
if (msg.networkPolicy !== undefined) networkPolicy = msg.networkPolicy;
|
|
215
285
|
try {
|
|
216
286
|
var mod = await loadModule(msg.source);
|
|
217
287
|
self.postMessage({
|
|
@@ -225,12 +295,13 @@ self.onmessage = async function (event) {
|
|
|
225
295
|
},
|
|
226
296
|
});
|
|
227
297
|
} catch (e) {
|
|
228
|
-
postError('load-result', msg.id, 'TOOL_EXECUTION_FAILED', String((e && e.message) || e));
|
|
298
|
+
postError('load-result', msg.id, (e && e.code) || 'TOOL_EXECUTION_FAILED', String((e && e.message) || e));
|
|
229
299
|
}
|
|
230
300
|
return;
|
|
231
301
|
}
|
|
232
302
|
|
|
233
303
|
if (msg.type === 'execute') {
|
|
304
|
+
if (msg.networkPolicy !== undefined) networkPolicy = msg.networkPolicy;
|
|
234
305
|
var stdout = [];
|
|
235
306
|
var stderr = [];
|
|
236
307
|
var originals = {
|
|
@@ -405,6 +476,11 @@ var BrowserSkillManager = class {
|
|
|
405
476
|
const contentDir = `${stagingRoot}/content`;
|
|
406
477
|
await fs.mkdir(contentDir);
|
|
407
478
|
await extractZipWeb(fs, data, contentDir);
|
|
479
|
+
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
480
|
+
if (await fs.exists(packFilePath)) return (await this.#installPack({
|
|
481
|
+
contentDir,
|
|
482
|
+
manifest: parseSkillPackManifest(await fs.readText(packFilePath))
|
|
483
|
+
}, source, stagingRoot))[0];
|
|
408
484
|
let skillDir = contentDir;
|
|
409
485
|
if (!await fs.exists(`${contentDir}/SKILL.md`)) {
|
|
410
486
|
const dirs = (await fs.list(contentDir)).filter((e) => e.type === "directory");
|
|
@@ -450,6 +526,77 @@ var BrowserSkillManager = class {
|
|
|
450
526
|
await removeDirQuiet(fs, stagingRoot);
|
|
451
527
|
}
|
|
452
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* 包集安装:逐技能走单技能管线(归一 → 汇总校验 → 落 managed root → manifest → digest 比对)。
|
|
531
|
+
* 任一失败整体回滚(删除本次已落目录;lockfile 在全部成功后才写入,天然零残留)。
|
|
532
|
+
*/
|
|
533
|
+
async #installPack(pack, source, stagingRoot) {
|
|
534
|
+
const fs = this.#fs;
|
|
535
|
+
const finalRoot = `${stagingRoot}/final`;
|
|
536
|
+
const installed = [];
|
|
537
|
+
try {
|
|
538
|
+
const versions = /* @__PURE__ */ new Map();
|
|
539
|
+
for (const entry of pack.manifest.skills) {
|
|
540
|
+
const skillDir = `${pack.contentDir}/${entry.name}`;
|
|
541
|
+
if (!await fs.exists(`${skillDir}/SKILL.md`)) throw new WebSkillError("INSTALL_FAILED", `Skill pack is missing a directory for skill "${entry.name}"`);
|
|
542
|
+
let name;
|
|
543
|
+
try {
|
|
544
|
+
const { metadata } = parseSkillMarkdown(await fs.readText(`${skillDir}/SKILL.md`));
|
|
545
|
+
name = metadata.name;
|
|
546
|
+
versions.set(name, typeof metadata["version"] === "string" ? metadata["version"] : void 0);
|
|
547
|
+
} catch (e) {
|
|
548
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md of pack entry "${entry.name}": ${messageOf$1(e)}`, e);
|
|
549
|
+
}
|
|
550
|
+
if (name !== entry.name) throw new WebSkillError("INSTALL_FAILED", `Skill pack entry "${entry.name}" does not match the SKILL.md name "${name}"`);
|
|
551
|
+
await copyDir(fs, skillDir, `${finalRoot}/${name}`);
|
|
552
|
+
}
|
|
553
|
+
const report = await validateSkills(fs, [finalRoot]);
|
|
554
|
+
if (!report.ok) {
|
|
555
|
+
const errors = report.issues.filter((i) => i.severity === "error");
|
|
556
|
+
throw new WebSkillError("INSTALL_FAILED", `Skill pack failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
557
|
+
}
|
|
558
|
+
const manifests = [];
|
|
559
|
+
for (const entry of pack.manifest.skills) {
|
|
560
|
+
const targetDir = `${this.#managedRoot}/${entry.name}`;
|
|
561
|
+
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
562
|
+
await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
|
|
563
|
+
installed.push(targetDir);
|
|
564
|
+
const manifest = await this.#createManifest(targetDir, {
|
|
565
|
+
name: entry.name,
|
|
566
|
+
...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
|
|
567
|
+
source
|
|
568
|
+
});
|
|
569
|
+
if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
|
|
570
|
+
manifests.push(manifest);
|
|
571
|
+
}
|
|
572
|
+
for (const manifest of manifests) await this.#upsertLockEntry(manifest.name, {
|
|
573
|
+
digest: manifest.integrity.digest,
|
|
574
|
+
installedAt: manifest.installedAt,
|
|
575
|
+
source
|
|
576
|
+
});
|
|
577
|
+
return manifests;
|
|
578
|
+
} catch (e) {
|
|
579
|
+
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
580
|
+
throw asInstallFailed(e);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* 浏览器导出(单技能与包集同构:始终产出含 webskill.skill-pack.json 的 zip 字节,
|
|
585
|
+
* 统一安装路径;保存/下载由应用层处理)。
|
|
586
|
+
*/
|
|
587
|
+
async exportArchive(names) {
|
|
588
|
+
const list = Array.isArray(names) ? names : [names];
|
|
589
|
+
if (list.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportArchive requires at least one skill name");
|
|
590
|
+
const roots = list.map((name) => {
|
|
591
|
+
if (!isValidSkillName(name)) throw new WebSkillError("EXPORT_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
|
|
592
|
+
return `${this.#managedRoot}/${name}`;
|
|
593
|
+
});
|
|
594
|
+
for (const [index, root] of roots.entries()) if (!await this.#fs.exists(root)) throw new WebSkillError("EXPORT_FAILED", `Skill "${list[index]}" is not installed`);
|
|
595
|
+
return exportSkills(this.#fs, {
|
|
596
|
+
roots,
|
|
597
|
+
manifestBuilder: (root) => this.#readManifest(root)
|
|
598
|
+
});
|
|
599
|
+
}
|
|
453
600
|
async uninstall(name) {
|
|
454
601
|
if (!isValidSkillName(name)) throw new WebSkillError("UNINSTALL_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
|
|
455
602
|
const targetDir = `${this.#managedRoot}/${name}`;
|
|
@@ -543,6 +690,8 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
543
690
|
#fs;
|
|
544
691
|
#workerFactory;
|
|
545
692
|
#capabilities;
|
|
693
|
+
#networkPolicy;
|
|
694
|
+
#approval;
|
|
546
695
|
#transpiler;
|
|
547
696
|
constructor(deps) {
|
|
548
697
|
this.#fs = deps.fs;
|
|
@@ -556,6 +705,11 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
556
705
|
writeArtifact: deps.capabilities?.writeArtifact ?? true,
|
|
557
706
|
confirm: deps.capabilities?.confirm ?? true
|
|
558
707
|
};
|
|
708
|
+
this.#networkPolicy = deps.networkPolicy ?? "deny-all";
|
|
709
|
+
this.#approval = new CapabilityApproval({
|
|
710
|
+
...deps.uiBridge ? { uiBridge: deps.uiBridge } : {},
|
|
711
|
+
...deps.approvalScope ? { scope: deps.approvalScope } : {}
|
|
712
|
+
});
|
|
559
713
|
}
|
|
560
714
|
/** 定位 scripts/<name>.js;.ts → TOOL_UNSUPPORTED(D4);冲突/缺失结构化报错 */
|
|
561
715
|
async #locateScript(skillRoot, scriptName) {
|
|
@@ -584,7 +738,8 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
584
738
|
const data = await this.#request(worker, {
|
|
585
739
|
type: "load",
|
|
586
740
|
id: "load-1",
|
|
587
|
-
source: script.source
|
|
741
|
+
source: script.source,
|
|
742
|
+
networkPolicy: this.#networkPolicy
|
|
588
743
|
});
|
|
589
744
|
if (!data.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${data.error?.message ?? "unknown error"}`);
|
|
590
745
|
return {
|
|
@@ -621,8 +776,9 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
621
776
|
skillName: context.skillName,
|
|
622
777
|
runId: context.runId,
|
|
623
778
|
source: script.source,
|
|
624
|
-
args
|
|
625
|
-
|
|
779
|
+
args,
|
|
780
|
+
networkPolicy: this.#networkPolicy
|
|
781
|
+
}, timeoutMs, (bridgeRequest) => this.#handleBridge(bridgeRequest, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
|
|
626
782
|
if (!response.ok) {
|
|
627
783
|
const stderrSummary = response.stderr?.length ? ` | stderr: ${response.stderr.join(" | ").slice(0, 500)}` : "";
|
|
628
784
|
return {
|
|
@@ -656,8 +812,8 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
656
812
|
worker.terminate();
|
|
657
813
|
}
|
|
658
814
|
}
|
|
659
|
-
/** 发消息并按 id 等回包;可选超时;onBridge
|
|
660
|
-
#request(worker, message, timeoutMs, onBridge) {
|
|
815
|
+
/** 发消息并按 id 等回包;可选超时;onBridge 处理执行期间的能力桥请求;onNetworkBlocked 收网络阻断通知 */
|
|
816
|
+
#request(worker, message, timeoutMs, onBridge, onNetworkBlocked) {
|
|
661
817
|
const id = String(message["id"]);
|
|
662
818
|
return new Promise((resolve, reject) => {
|
|
663
819
|
let settled = false;
|
|
@@ -670,6 +826,10 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
670
826
|
worker.addEventListener("message", (event) => {
|
|
671
827
|
const data = event.data;
|
|
672
828
|
if (!data || typeof data !== "object") return;
|
|
829
|
+
if (data.type === "network-blocked") {
|
|
830
|
+
if (typeof data.host === "string") onNetworkBlocked?.(data.host);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
673
833
|
if (data.type === "bridge" && onBridge) {
|
|
674
834
|
const request = parseBridgeRequest(data.request);
|
|
675
835
|
const respond = (response) => worker.postMessage({
|
|
@@ -691,20 +851,36 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
691
851
|
worker.postMessage(message);
|
|
692
852
|
});
|
|
693
853
|
}
|
|
694
|
-
/**
|
|
854
|
+
/** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
|
|
695
855
|
async #handleBridge(request, context) {
|
|
696
|
-
const
|
|
856
|
+
const gate = async (capability, message, details) => {
|
|
857
|
+
const decision = await this.#approval.authorize({
|
|
858
|
+
runId: context.runId,
|
|
859
|
+
capability,
|
|
860
|
+
mode: this.#capabilities[capability],
|
|
861
|
+
message,
|
|
862
|
+
...details !== void 0 ? { details } : {}
|
|
863
|
+
});
|
|
864
|
+
if (decision === "allowed") return void 0;
|
|
865
|
+
return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
|
|
866
|
+
};
|
|
697
867
|
try {
|
|
698
868
|
switch (request.kind) {
|
|
699
|
-
case "readReference":
|
|
700
|
-
|
|
869
|
+
case "readReference": {
|
|
870
|
+
const denied = await gate("readReference", `Script "${context.skillName}" wants to read reference "${request.path}"`, { path: request.path });
|
|
871
|
+
if (denied) return denied;
|
|
701
872
|
return {
|
|
702
873
|
id: request.id,
|
|
703
874
|
ok: true,
|
|
704
875
|
value: await context.readReference(request.path)
|
|
705
876
|
};
|
|
877
|
+
}
|
|
706
878
|
case "writeArtifact": {
|
|
707
|
-
|
|
879
|
+
const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
|
|
880
|
+
path: request.path,
|
|
881
|
+
mimeType: request.mimeType
|
|
882
|
+
});
|
|
883
|
+
if (denied) return denied;
|
|
708
884
|
const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
|
|
709
885
|
const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
|
|
710
886
|
return {
|
|
@@ -713,19 +889,50 @@ var BrowserWorkerScriptExecutor = class {
|
|
|
713
889
|
value: artifact
|
|
714
890
|
};
|
|
715
891
|
}
|
|
716
|
-
case "confirm":
|
|
717
|
-
if (!
|
|
892
|
+
case "confirm": {
|
|
893
|
+
if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
|
|
894
|
+
const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
|
|
895
|
+
if (denied) return denied;
|
|
718
896
|
return {
|
|
719
897
|
id: request.id,
|
|
720
898
|
ok: true,
|
|
721
899
|
value: await context.confirm(request.message)
|
|
722
900
|
};
|
|
901
|
+
}
|
|
723
902
|
}
|
|
724
903
|
} catch (e) {
|
|
725
904
|
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
|
|
726
905
|
}
|
|
727
906
|
}
|
|
728
907
|
};
|
|
908
|
+
/**
|
|
909
|
+
* navigator.webskill 显式装配:OPFS 默认存储 + BrowserWorkerScriptExecutor +
|
|
910
|
+
* BrowserSkillManager → createWebSkillApi;target 提供时挂 target.navigator
|
|
911
|
+
* (缺失则补空对象),否则挂真实 navigator.webskill。
|
|
912
|
+
* target 可注入(测试);返回值即门面本体(挂载只是副作用)。
|
|
913
|
+
*/
|
|
914
|
+
function installWebSkillNavigator(config) {
|
|
915
|
+
const roots = config.roots ?? ["/skills"];
|
|
916
|
+
const fs = config.fs ?? new OpfsProvider();
|
|
917
|
+
const executor = new BrowserWorkerScriptExecutor({
|
|
918
|
+
fs,
|
|
919
|
+
...config.typescript ? { typescript: config.typescript } : {}
|
|
920
|
+
});
|
|
921
|
+
const skillManager = new BrowserSkillManager({
|
|
922
|
+
fs,
|
|
923
|
+
managedRoot: config.managedRoot ?? roots[0] ?? "/skills"
|
|
924
|
+
});
|
|
925
|
+
const api = createWebSkillApi({
|
|
926
|
+
fs,
|
|
927
|
+
roots,
|
|
928
|
+
llm: config.llm,
|
|
929
|
+
executor,
|
|
930
|
+
skillManager
|
|
931
|
+
});
|
|
932
|
+
const nav = config.target ? config.target.navigator ??= {} : navigator;
|
|
933
|
+
nav["webskill"] = api;
|
|
934
|
+
return api;
|
|
935
|
+
}
|
|
729
936
|
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
730
937
|
const isNonEmptyString = (v) => typeof v === "string" && v !== "";
|
|
731
938
|
/** Worker 侧入站校验:非法消息返回 undefined(忽略) */
|
|
@@ -1234,4 +1441,4 @@ var WorkerRuntimeClient = class {
|
|
|
1234
1441
|
};
|
|
1235
1442
|
|
|
1236
1443
|
//#endregion
|
|
1237
|
-
export { BrowserSkillManager, BrowserWorkerScriptExecutor, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, extractZipWeb, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
|
|
1444
|
+
export { BrowserSkillManager, BrowserWorkerScriptExecutor, OpfsProvider, TsTranspiler, WORKER_BOOTSTRAP_SOURCE, WorkerRuntimeClient, WorkerUiBridge, bridgeError, extractZipWeb, installWebSkillNavigator, isOpfsAvailable, parseBridgeRequest, parseMainMessage, parseWorkerEvent, sha256HexWeb, startWorkerRuntimeHost };
|