@webskill/sdk 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/browser.d.ts +8 -4
- package/dist/browser.js +69 -39
- package/dist/{dist-DxGyAznR.js → dist-CfBOjJ4p.js} +46 -3
- package/dist/{dist-DZobLFh6.js → dist-DuGN5x0v.js} +123 -20
- package/dist/{dist-Cm_j6Jol.js → dist-LIFS3ZjI.js} +204 -74
- package/dist/{dist-DS1sfgHa.js → dist-fZFZaf43.js} +145 -7
- package/dist/governance.d.ts +13 -4
- package/dist/governance.js +35 -5
- package/dist/{index-CPuwsnmB.d.ts → index-Bun1aEf4.d.ts} +20 -3
- package/dist/{index-CqMuvcb2.d.ts → index-COuOu6gw.d.ts} +19 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +9 -3
- package/dist/node.d.ts +3 -3
- package/dist/node.js +3 -3
- package/dist/sandboxWorkerEntry.d.ts +1 -0
- package/dist/sandboxWorkerEntry.js +231 -0
- package/dist/{testing-CbM6rJ-E.js → testing-BmR48Pn1.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CgNC-oQu-Dq_A1yA-.d.ts → types-CgNC-oQu-DEcIBJKG.d.ts} +39 -3
- package/dist/ui-react.d.ts +1 -1
- package/dist/ui-react.js +1 -1
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +1 -1
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,8 +16,11 @@ governance.
|
|
|
16
16
|
(missing-parameter forms, confirmations, authorization prompts), and
|
|
17
17
|
resumable interrupted runs.
|
|
18
18
|
- **Script sandbox** — `worker_threads` sandbox for Node, Web Worker sandbox for
|
|
19
|
-
the browser. Deny-by-default network policy,
|
|
20
|
-
timeouts kill runaway scripts.
|
|
19
|
+
the browser. Deny-by-default network policy, deny-by-default builtin-module
|
|
20
|
+
allowlist, per-capability forced approval, timeouts kill runaway scripts.
|
|
21
|
+
**Honest note:** these executors reduce the capability surface — they are NOT
|
|
22
|
+
a security boundary. Scripts share the host process/origin and bypasses
|
|
23
|
+
exist; never rely on them to isolate untrusted scripts.
|
|
21
24
|
- **`navigator.webskill`** — a browser facade (`discover` / `read` / `validate` /
|
|
22
25
|
`run` / `install` / `uninstall`) assembled explicitly in one call.
|
|
23
26
|
- **MCP** — call page-provided tools and consume page-declared dynamic skills
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { C as FileStat, I as SkillInstallSource, K as VerifyResult, W as SkillsLockfile, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, w as FileSystemProvider, y as ArchiveLimits, z as SkillManifest } from "./types-CgNC-oQu-DEcIBJKG.js";
|
|
2
|
+
import { N as NetworkPolicy, St as parseBridgeRequest, X as ScriptExecutionContext, Z as ScriptExecutor, ct as WebSkillApi, d as BridgeCapabilities, dt as bridgeError, et as ToolDefinition, m as BridgeResponse, nt as ToolResult, p as BridgeRequest, u as ApprovalScope, v as ExternalSkillProvider, y as ExternalToolSource } from "./index-COuOu6gw.js";
|
|
3
3
|
//#region ../browser/dist/index.d.ts
|
|
4
4
|
//#region src/fs/featureDetection.d.ts
|
|
5
5
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -83,6 +83,10 @@ declare class BrowserSkillManager {
|
|
|
83
83
|
fs: FileSystemProvider;
|
|
84
84
|
managedRoot: string;
|
|
85
85
|
fetchImpl?: typeof fetch;
|
|
86
|
+
/** 归档体积三重上限(缺省 DEFAULT_ARCHIVE_LIMITS) */
|
|
87
|
+
archiveLimits?: ArchiveLimits;
|
|
88
|
+
/** install/uninstall 成功后的变更回调(宿主接线缓存失效,如 WebSkillRuntime.invalidate) */
|
|
89
|
+
onChanged?: () => void;
|
|
86
90
|
});
|
|
87
91
|
install(source: SkillInstallSource, options?: {
|
|
88
92
|
expectedSha256?: string;
|
|
@@ -99,10 +103,10 @@ declare class BrowserSkillManager {
|
|
|
99
103
|
//#endregion
|
|
100
104
|
//#region src/skillManagement/zipExtract.d.ts
|
|
101
105
|
/**
|
|
102
|
-
* zip 解包(fflate
|
|
106
|
+
* zip 解包(fflate 流式,单条目/总解压体积上限,浏览器无依赖)。
|
|
103
107
|
* 每个条目路径过 resolveInsideRoot:拒绝 `..` 段、绝对路径、反斜杠穿越(与 node 规则一致)。
|
|
104
108
|
*/
|
|
105
|
-
declare function extractZipWeb(fs: FileSystemProvider, data: Uint8Array, destRoot: string): Promise<void>;
|
|
109
|
+
declare function extractZipWeb(fs: FileSystemProvider, data: Uint8Array, destRoot: string, limits?: ArchiveLimits): Promise<void>;
|
|
106
110
|
//#endregion
|
|
107
111
|
//#region src/skillManagement/checksumWeb.d.ts
|
|
108
112
|
/** WebCrypto sha256(浏览器/Worker 通用;Node 20+ 全局 crypto 同样可用) */
|
package/dist/browser.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-
|
|
3
|
-
import { n as MockLlmClient } from "./testing-
|
|
4
|
-
import { unzipSync } from "fflate";
|
|
1
|
+
import { A as validateSkills, C as parseSkillPackManifest, O as resolveInsideRoot, S as parseSkillMarkdown, c as SkillDiscovery, f as buildCatalog, 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 { A as mergeCatalogEntries, M as normalizeToolContent, N as parseBridgeRequest, S as bridgeError, T as createWebSkillApi, c as FsArtifactStore, h as ProgressiveRouter, i as AgentLoop, j as networkUrlHost, k as isNetworkAllowed, l as FsMemoryStore, m as OpenAiCompatibleClient, o as CapabilityApproval, u as FsRunSnapshotStore, y as RUN_SNAPSHOT_SCHEMA_VERSION } from "./dist-DuGN5x0v.js";
|
|
3
|
+
import { n as MockLlmClient } from "./testing-BmR48Pn1.js";
|
|
5
4
|
|
|
6
5
|
//#region ../browser/dist/index.js
|
|
7
6
|
/** 检测当前环境是否可用 OPFS(navigator.storage.getDirectory) */
|
|
@@ -421,17 +420,11 @@ async function removeDirQuiet(fs, dir) {
|
|
|
421
420
|
} catch {}
|
|
422
421
|
}
|
|
423
422
|
/**
|
|
424
|
-
* zip 解包(fflate
|
|
423
|
+
* zip 解包(fflate 流式,单条目/总解压体积上限,浏览器无依赖)。
|
|
425
424
|
* 每个条目路径过 resolveInsideRoot:拒绝 `..` 段、绝对路径、反斜杠穿越(与 node 规则一致)。
|
|
426
425
|
*/
|
|
427
|
-
async function extractZipWeb(fs, data, destRoot) {
|
|
428
|
-
|
|
429
|
-
try {
|
|
430
|
-
entries = unzipSync(data);
|
|
431
|
-
} catch (e) {
|
|
432
|
-
throw new WebSkillError("INSTALL_FAILED", `Failed to read zip archive: ${e instanceof Error ? e.message : String(e)}`, e);
|
|
433
|
-
}
|
|
434
|
-
for (const [entryPath, content] of Object.entries(entries)) {
|
|
426
|
+
async function extractZipWeb(fs, data, destRoot, limits) {
|
|
427
|
+
for (const [entryPath, content] of await unzipWithLimits(data, limits)) {
|
|
435
428
|
const target = resolveInsideRoot(destRoot, entryPath);
|
|
436
429
|
if (entryPath.endsWith("/")) await fs.mkdir(target);
|
|
437
430
|
else await fs.writeBinary(target, content);
|
|
@@ -450,10 +443,14 @@ var BrowserSkillManager = class {
|
|
|
450
443
|
#fs;
|
|
451
444
|
#managedRoot;
|
|
452
445
|
#fetchImpl;
|
|
446
|
+
#archiveLimits;
|
|
447
|
+
#onChanged;
|
|
453
448
|
constructor(deps) {
|
|
454
449
|
this.#fs = deps.fs;
|
|
455
450
|
this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
|
|
456
451
|
this.#fetchImpl = deps.fetchImpl;
|
|
452
|
+
this.#archiveLimits = deps.archiveLimits;
|
|
453
|
+
this.#onChanged = deps.onChanged;
|
|
457
454
|
}
|
|
458
455
|
async install(source, options) {
|
|
459
456
|
const fs = this.#fs;
|
|
@@ -470,7 +467,7 @@ var BrowserSkillManager = class {
|
|
|
470
467
|
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf$1(e)}`, e);
|
|
471
468
|
}
|
|
472
469
|
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
473
|
-
data =
|
|
470
|
+
data = await readResponseWithLimit(res, this.#archiveLimits);
|
|
474
471
|
} else data = new Uint8Array(source.data);
|
|
475
472
|
if (options?.expectedSha256 !== void 0) {
|
|
476
473
|
const actual = await sha256HexWeb(data);
|
|
@@ -478,7 +475,7 @@ var BrowserSkillManager = class {
|
|
|
478
475
|
}
|
|
479
476
|
const contentDir = `${stagingRoot}/content`;
|
|
480
477
|
await fs.mkdir(contentDir);
|
|
481
|
-
await extractZipWeb(fs, data, contentDir);
|
|
478
|
+
await extractZipWeb(fs, data, contentDir, this.#archiveLimits);
|
|
482
479
|
const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
|
|
483
480
|
if (await fs.exists(packFilePath)) return (await this.#installPack({
|
|
484
481
|
contentDir,
|
|
@@ -508,20 +505,33 @@ var BrowserSkillManager = class {
|
|
|
508
505
|
const errors = report.issues.filter((i) => i.severity === "error");
|
|
509
506
|
throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
|
|
510
507
|
}
|
|
511
|
-
|
|
512
|
-
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
513
|
-
await copyDir(fs, finalDir, targetDir);
|
|
514
|
-
const manifest = await this.#createManifest(targetDir, {
|
|
508
|
+
const manifest = await this.#createManifest(finalDir, {
|
|
515
509
|
name,
|
|
516
510
|
...version ? { version } : {},
|
|
517
511
|
source
|
|
518
512
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
513
|
+
targetDir = `${this.#managedRoot}/${name}`;
|
|
514
|
+
const backupDir = `${stagingRoot}/backup`;
|
|
515
|
+
const hadPrevious = await fs.exists(targetDir);
|
|
516
|
+
if (hadPrevious) await copyDir(fs, targetDir, backupDir);
|
|
517
|
+
let committed = false;
|
|
518
|
+
try {
|
|
519
|
+
if (hadPrevious) await fs.remove(targetDir, { recursive: true });
|
|
520
|
+
await copyDir(fs, finalDir, targetDir);
|
|
521
|
+
committed = true;
|
|
522
|
+
await this.#upsertLockEntry(name, {
|
|
523
|
+
digest: manifest.integrity.digest,
|
|
524
|
+
installedAt: manifest.installedAt,
|
|
525
|
+
source
|
|
526
|
+
});
|
|
527
|
+
} catch (e) {
|
|
528
|
+
if (committed) await removeDirQuiet(fs, targetDir);
|
|
529
|
+
if (hadPrevious) await copyDir(fs, backupDir, targetDir);
|
|
530
|
+
targetDir = void 0;
|
|
531
|
+
throw e;
|
|
532
|
+
}
|
|
524
533
|
targetDir = void 0;
|
|
534
|
+
this.#onChanged?.();
|
|
525
535
|
return manifest;
|
|
526
536
|
} catch (e) {
|
|
527
537
|
if (targetDir) await removeDirQuiet(fs, targetDir);
|
|
@@ -562,11 +572,7 @@ var BrowserSkillManager = class {
|
|
|
562
572
|
}
|
|
563
573
|
const manifests = [];
|
|
564
574
|
for (const entry of pack.manifest.skills) {
|
|
565
|
-
const
|
|
566
|
-
if (await fs.exists(targetDir)) await fs.remove(targetDir, { recursive: true });
|
|
567
|
-
await copyDir(fs, `${finalRoot}/${entry.name}`, targetDir);
|
|
568
|
-
installed.push(targetDir);
|
|
569
|
-
const manifest = await this.#createManifest(targetDir, {
|
|
575
|
+
const manifest = await this.#createManifest(`${finalRoot}/${entry.name}`, {
|
|
570
576
|
name: entry.name,
|
|
571
577
|
...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
|
|
572
578
|
source
|
|
@@ -574,11 +580,36 @@ var BrowserSkillManager = class {
|
|
|
574
580
|
if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
|
|
575
581
|
manifests.push(manifest);
|
|
576
582
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
583
|
+
const swapped = [];
|
|
584
|
+
try {
|
|
585
|
+
for (const manifest of manifests) {
|
|
586
|
+
const targetDir = `${this.#managedRoot}/${manifest.name}`;
|
|
587
|
+
const backupDir = `${stagingRoot}/backup/${manifest.name}`;
|
|
588
|
+
if (await fs.exists(targetDir)) {
|
|
589
|
+
await copyDir(fs, targetDir, backupDir);
|
|
590
|
+
await fs.remove(targetDir, { recursive: true });
|
|
591
|
+
}
|
|
592
|
+
await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
|
|
593
|
+
installed.push(targetDir);
|
|
594
|
+
swapped.push({
|
|
595
|
+
targetDir,
|
|
596
|
+
backupDir
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
for (const manifest of manifests) await this.#upsertLockEntry(manifest.name, {
|
|
600
|
+
digest: manifest.integrity.digest,
|
|
601
|
+
installedAt: manifest.installedAt,
|
|
602
|
+
source
|
|
603
|
+
});
|
|
604
|
+
} catch (e) {
|
|
605
|
+
for (const { targetDir, backupDir } of swapped) {
|
|
606
|
+
await removeDirQuiet(fs, targetDir);
|
|
607
|
+
if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
|
|
608
|
+
}
|
|
609
|
+
installed.length = 0;
|
|
610
|
+
throw e;
|
|
611
|
+
}
|
|
612
|
+
this.#onChanged?.();
|
|
582
613
|
return manifests;
|
|
583
614
|
} catch (e) {
|
|
584
615
|
for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
|
|
@@ -609,6 +640,7 @@ var BrowserSkillManager = class {
|
|
|
609
640
|
try {
|
|
610
641
|
await this.#fs.remove(targetDir, { recursive: true });
|
|
611
642
|
await this.#removeLockEntry(name);
|
|
643
|
+
this.#onChanged?.();
|
|
612
644
|
} catch (e) {
|
|
613
645
|
throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf$1(e)}`, e);
|
|
614
646
|
}
|
|
@@ -617,11 +649,9 @@ var BrowserSkillManager = class {
|
|
|
617
649
|
if (!isValidSkillName(name)) throw new WebSkillError("INTEGRITY_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
|
|
618
650
|
const manifest = await this.#readManifest(`${this.#managedRoot}/${name}`);
|
|
619
651
|
const actualHashes = /* @__PURE__ */ new Map();
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
return verifyManifest(manifest, actualHashes);
|
|
652
|
+
const actualFiles = await listFiles(this.#fs, `${this.#managedRoot}/${name}`);
|
|
653
|
+
for (const rel of actualFiles) actualHashes.set(rel, await sha256HexWeb(await this.#fs.readBinary(`${this.#managedRoot}/${name}/${rel}`)));
|
|
654
|
+
return verifyManifest(manifest, actualHashes, actualFiles);
|
|
625
655
|
}
|
|
626
656
|
async listInstalled() {
|
|
627
657
|
const path = lockfilePath(this.#managedRoot);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { u as WebSkillError } from "./dist-fZFZaf43.js";
|
|
2
2
|
|
|
3
3
|
//#region ../ui/dist/index.js
|
|
4
4
|
/** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
|
|
@@ -1040,8 +1040,7 @@ var LitRendererBridge = class {
|
|
|
1040
1040
|
values: formValues
|
|
1041
1041
|
}
|
|
1042
1042
|
});
|
|
1043
|
-
|
|
1044
|
-
resolve(response);
|
|
1043
|
+
resolve(this.#decodeValue(input, response));
|
|
1045
1044
|
}, { version: "v0.9.1" });
|
|
1046
1045
|
processor.onSurfaceCreated((surface) => {
|
|
1047
1046
|
const el = this.#doc.createElement("a2ui-surface");
|
|
@@ -1052,6 +1051,50 @@ var LitRendererBridge = class {
|
|
|
1052
1051
|
processor.processMessages(messages);
|
|
1053
1052
|
});
|
|
1054
1053
|
}
|
|
1054
|
+
/**
|
|
1055
|
+
* 按 InteractionRequest.type 解码提交值(绑定模型原样回传的是表单对象):
|
|
1056
|
+
* confirm 仅 confirmed===true 批准;select 还原原始 option 值(非字符串值经 JSON 编码比对);
|
|
1057
|
+
* form 的 number 字段转 number;ask 取 answer;authorize 提交即批准。
|
|
1058
|
+
*/
|
|
1059
|
+
#decodeValue(input, response) {
|
|
1060
|
+
if (response.cancelled) return response;
|
|
1061
|
+
const values = response.value;
|
|
1062
|
+
switch (input.type) {
|
|
1063
|
+
case "confirm": return {
|
|
1064
|
+
...response,
|
|
1065
|
+
value: values?.confirmed === true
|
|
1066
|
+
};
|
|
1067
|
+
case "ask": return {
|
|
1068
|
+
...response,
|
|
1069
|
+
value: values?.answer
|
|
1070
|
+
};
|
|
1071
|
+
case "select": {
|
|
1072
|
+
const raw = values?.selected;
|
|
1073
|
+
const option = input.options.find((o) => o.value === raw || JSON.stringify(o.value) === raw);
|
|
1074
|
+
return {
|
|
1075
|
+
...response,
|
|
1076
|
+
value: option ? option.value : raw
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
case "form": {
|
|
1080
|
+
if (typeof values !== "object" || values === null) return response;
|
|
1081
|
+
const out = { ...values };
|
|
1082
|
+
for (const field of input.fields) if (field.type === "number" && out[field.name] !== void 0) {
|
|
1083
|
+
const n = Number(out[field.name]);
|
|
1084
|
+
if (!Number.isNaN(n)) out[field.name] = n;
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
...response,
|
|
1088
|
+
value: out
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
case "authorize": return {
|
|
1092
|
+
...response,
|
|
1093
|
+
value: true
|
|
1094
|
+
};
|
|
1095
|
+
default: return response;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1055
1098
|
};
|
|
1056
1099
|
|
|
1057
1100
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as validateSkills, O as resolveInsideRoot, S as parseSkillMarkdown, T as renderAvailableSkillsXml, c as SkillDiscovery, d as assertSafePathSegment, f as buildCatalog, l as SkillReader, u as WebSkillError } from "./dist-fZFZaf43.js";
|
|
2
2
|
import { t as MemoryArtifactStore } from "./memoryArtifactStore-C9lFVqPF-yFz6yJj0.js";
|
|
3
3
|
|
|
4
4
|
//#region ../runtime/dist/index.js
|
|
@@ -1050,6 +1050,7 @@ var AgentLoop = class {
|
|
|
1050
1050
|
maxTurns: config.maxTurns ?? 10,
|
|
1051
1051
|
totalTimeoutMs: config.totalTimeoutMs ?? 12e4,
|
|
1052
1052
|
toolTimeoutMs: config.toolTimeoutMs ?? 3e4,
|
|
1053
|
+
toolResultMaxBytes: config.toolResultMaxBytes ?? 1e5,
|
|
1053
1054
|
temperature: config.temperature,
|
|
1054
1055
|
renderResult: config.renderResult
|
|
1055
1056
|
};
|
|
@@ -1089,7 +1090,12 @@ var AgentLoop = class {
|
|
|
1089
1090
|
interactionSeq: 0,
|
|
1090
1091
|
messages: [],
|
|
1091
1092
|
turn: 0,
|
|
1092
|
-
renderBlocks: []
|
|
1093
|
+
renderBlocks: [],
|
|
1094
|
+
startMs,
|
|
1095
|
+
pausedMs: 0,
|
|
1096
|
+
maxTurns: this.#config.maxTurns,
|
|
1097
|
+
totalTimeoutMs: this.#config.totalTimeoutMs,
|
|
1098
|
+
controller: new AbortController()
|
|
1093
1099
|
};
|
|
1094
1100
|
if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
|
|
1095
1101
|
const route = this.#deps.catalogFilter ? {
|
|
@@ -1117,21 +1123,42 @@ var AgentLoop = class {
|
|
|
1117
1123
|
content: input.userPrompt
|
|
1118
1124
|
}];
|
|
1119
1125
|
try {
|
|
1120
|
-
return await this.#turnLoop(state,
|
|
1126
|
+
return await this.#turnLoop(state, 1, externalSpecs);
|
|
1121
1127
|
} catch (e) {
|
|
1122
1128
|
if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
|
|
1123
1129
|
throw e;
|
|
1124
1130
|
}
|
|
1125
1131
|
}
|
|
1132
|
+
/** 总时长消耗(wall - 交互等待);交互期间不计入 totalTimeout */
|
|
1133
|
+
#elapsed(state) {
|
|
1134
|
+
return Date.parse(state.now()) - state.startMs - state.pausedMs;
|
|
1135
|
+
}
|
|
1136
|
+
/** 重设硬期限定时器(剩余 = totalTimeoutMs - 已消耗;耗尽立即 abort) */
|
|
1137
|
+
#armDeadline(state) {
|
|
1138
|
+
if (state.timer) clearTimeout(state.timer);
|
|
1139
|
+
const remaining = state.totalTimeoutMs - this.#elapsed(state);
|
|
1140
|
+
if (remaining <= 0) {
|
|
1141
|
+
state.controller.abort();
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
state.timer = setTimeout(() => state.controller.abort(), remaining);
|
|
1145
|
+
}
|
|
1146
|
+
#disarmDeadline(state) {
|
|
1147
|
+
if (state.timer) {
|
|
1148
|
+
clearTimeout(state.timer);
|
|
1149
|
+
state.timer = void 0;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1126
1152
|
/** 主循环(run 从第 1 轮、resume 从快照轮次续跑;totalTimeout 以 startedAt 续算) */
|
|
1127
|
-
async #turnLoop(state,
|
|
1153
|
+
async #turnLoop(state, startTurn, externalSpecs) {
|
|
1128
1154
|
const finish = (status, reason, output, errorCode) => this.#finish(state, status, reason, output, errorCode);
|
|
1129
1155
|
const { llm } = this.#deps;
|
|
1130
1156
|
const { trace, messages } = state;
|
|
1157
|
+
this.#armDeadline(state);
|
|
1131
1158
|
for (let turn = startTurn;; turn++) {
|
|
1132
1159
|
state.turn = turn;
|
|
1133
|
-
if (turn >
|
|
1134
|
-
if (
|
|
1160
|
+
if (turn > state.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${state.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
|
|
1161
|
+
if (this.#elapsed(state) > state.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1135
1162
|
const toolSpecs = [
|
|
1136
1163
|
toLlmToolSpec(READ_SKILL_FILE_TOOL),
|
|
1137
1164
|
...this.#deps.uiBridge ? [toLlmToolSpec(ASK_USER_TOOL)] : [],
|
|
@@ -1149,14 +1176,17 @@ var AgentLoop = class {
|
|
|
1149
1176
|
model: this.#deps.model,
|
|
1150
1177
|
messages,
|
|
1151
1178
|
tools: toolSpecs,
|
|
1152
|
-
temperature: this.#config.temperature
|
|
1179
|
+
temperature: this.#config.temperature,
|
|
1180
|
+
signal: state.controller.signal
|
|
1153
1181
|
}) : await llm.complete({
|
|
1154
1182
|
model: this.#deps.model,
|
|
1155
1183
|
messages,
|
|
1156
1184
|
tools: toolSpecs,
|
|
1157
|
-
temperature: this.#config.temperature
|
|
1185
|
+
temperature: this.#config.temperature,
|
|
1186
|
+
signal: state.controller.signal
|
|
1158
1187
|
});
|
|
1159
1188
|
} catch (e) {
|
|
1189
|
+
if (state.controller.signal.aborted) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${state.totalTimeoutMs}ms`, "RUN_TIMEOUT");
|
|
1160
1190
|
const code = e instanceof WebSkillError && (e.code === "LLM_UNAVAILABLE" || e.code === "LLM_REQUEST_FAILED") ? e.code : "LLM_REQUEST_FAILED";
|
|
1161
1191
|
return finish("failed", "llm-error", messageOf$1(e), code);
|
|
1162
1192
|
}
|
|
@@ -1183,13 +1213,14 @@ var AgentLoop = class {
|
|
|
1183
1213
|
messages.push({
|
|
1184
1214
|
role: "tool",
|
|
1185
1215
|
toolCallId: call.id,
|
|
1186
|
-
content:
|
|
1216
|
+
content: await this.#serializeToolResult(call, result, state)
|
|
1187
1217
|
});
|
|
1188
1218
|
}
|
|
1189
1219
|
}
|
|
1190
1220
|
}
|
|
1191
1221
|
/** 统一终态处理:completed/failed/cancelled;终态即删快照(快照是续命机制,审计归治理) */
|
|
1192
1222
|
async #finish(state, status, reason, output, errorCode) {
|
|
1223
|
+
this.#disarmDeadline(state);
|
|
1193
1224
|
const { run, trace } = state;
|
|
1194
1225
|
run.status = status;
|
|
1195
1226
|
run.terminationReason = reason;
|
|
@@ -1293,7 +1324,12 @@ var AgentLoop = class {
|
|
|
1293
1324
|
interactionSeq: 0,
|
|
1294
1325
|
messages: snapshot.messages.map((m) => ({ ...m })),
|
|
1295
1326
|
turn: snapshot.turn,
|
|
1296
|
-
renderBlocks: []
|
|
1327
|
+
renderBlocks: [],
|
|
1328
|
+
startMs,
|
|
1329
|
+
pausedMs: 0,
|
|
1330
|
+
maxTurns: snapshot.config.maxTurns,
|
|
1331
|
+
totalTimeoutMs: snapshot.config.totalTimeoutMs,
|
|
1332
|
+
controller: new AbortController()
|
|
1297
1333
|
};
|
|
1298
1334
|
if (this.#deps.hooks && !this.#deps.hooks.onWarning) this.#deps.hooks.onWarning = (message) => trace.record("run.warning", { message });
|
|
1299
1335
|
trace.record("run.resumed", { data: {
|
|
@@ -1364,7 +1400,7 @@ var AgentLoop = class {
|
|
|
1364
1400
|
}
|
|
1365
1401
|
}))).flat();
|
|
1366
1402
|
try {
|
|
1367
|
-
return await this.#turnLoop(state,
|
|
1403
|
+
return await this.#turnLoop(state, snapshot.turn + 1, externalSpecs);
|
|
1368
1404
|
} catch (e) {
|
|
1369
1405
|
if (e instanceof WebSkillError && e.code === "RUN_FAILED") return this.#finish(state, "failed", "hook-error", messageOf$1(e), "RUN_FAILED");
|
|
1370
1406
|
throw e;
|
|
@@ -1431,8 +1467,18 @@ var AgentLoop = class {
|
|
|
1431
1467
|
* 取消 → RunTerminated(cancelled);超时 → RunTerminated(interaction-timeout)。
|
|
1432
1468
|
*/
|
|
1433
1469
|
async #interact(state, request, traceData) {
|
|
1470
|
+
if (!this.#deps.uiBridge) throw new Error("interact() requires a UiBridge");
|
|
1471
|
+
this.#disarmDeadline(state);
|
|
1472
|
+
const waitStartMs = Date.parse(state.now());
|
|
1473
|
+
try {
|
|
1474
|
+
return await this.#interactInner(state, request, traceData);
|
|
1475
|
+
} finally {
|
|
1476
|
+
state.pausedMs += Date.parse(state.now()) - waitStartMs;
|
|
1477
|
+
this.#armDeadline(state);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
async #interactInner(state, request, traceData) {
|
|
1434
1481
|
const bridge = this.#deps.uiBridge;
|
|
1435
|
-
if (!bridge) throw new Error("interact() requires a UiBridge");
|
|
1436
1482
|
const { run } = state;
|
|
1437
1483
|
run.status = "interrupted";
|
|
1438
1484
|
run.interruptExpiresAt = new Date(Date.parse(state.now()) + this.#policy.interactionTimeoutMs).toISOString();
|
|
@@ -1560,6 +1606,7 @@ var AgentLoop = class {
|
|
|
1560
1606
|
if (typeof pathArg !== "string") return toolError("TOOL_EXECUTION_FAILED", "read_skill_file \"path\" must be a string");
|
|
1561
1607
|
if (skillName.startsWith("mcp://")) return this.#handleReadExternalSkill(skillName, pathArg, state);
|
|
1562
1608
|
try {
|
|
1609
|
+
if (await this.#guardDenied("canRead", skillName)) return toolError("SKILL_DISABLED", `Skill "${skillName}" is blocked by the skill state guard (read)`);
|
|
1563
1610
|
const text = await state.reader.readSkillFile(skillName, pathArg);
|
|
1564
1611
|
let note = "";
|
|
1565
1612
|
if (pathArg === "SKILL.md" && !state.activated.has(skillName)) note = await this.#activateSkill(skillName, state, void 0, text);
|
|
@@ -1577,6 +1624,7 @@ var AgentLoop = class {
|
|
|
1577
1624
|
}
|
|
1578
1625
|
/** 经外部技能提供者读取技能文档/资源;skillKey 为技能名或 mcp:// URI */
|
|
1579
1626
|
async #handleReadExternalSkill(skillKey, pathArg, state) {
|
|
1627
|
+
const failures = [];
|
|
1580
1628
|
for (const provider of this.#deps.skillProviders ?? []) try {
|
|
1581
1629
|
let name = skillKey;
|
|
1582
1630
|
if (skillKey.startsWith("mcp://")) {
|
|
@@ -1610,13 +1658,42 @@ var AgentLoop = class {
|
|
|
1610
1658
|
text: await provider.readFile(name, pathArg)
|
|
1611
1659
|
}]
|
|
1612
1660
|
};
|
|
1613
|
-
} catch {
|
|
1661
|
+
} catch (e) {
|
|
1662
|
+
failures.push(messageOf$1(e));
|
|
1614
1663
|
continue;
|
|
1615
1664
|
}
|
|
1616
|
-
|
|
1665
|
+
const detail = failures.length > 0 ? ` (provider errors: ${failures.join("; ")})` : "";
|
|
1666
|
+
return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* 工具结果回喂序列化:超过 toolResultMaxBytes(默认 100KB)时头尾保留截断,
|
|
1670
|
+
* 完整内容经 artifactStore 落 artifact,回喂摘要含 artifact id。
|
|
1671
|
+
*/
|
|
1672
|
+
async #serializeToolResult(call, result, state) {
|
|
1673
|
+
const text = JSON.stringify(result);
|
|
1674
|
+
const max = this.#config.toolResultMaxBytes;
|
|
1675
|
+
if (text.length <= max) return text;
|
|
1676
|
+
const artifact = await this.#deps.artifactStore.createTextArtifact({
|
|
1677
|
+
runId: state.runId,
|
|
1678
|
+
path: `tool-results/${call.id}.json`,
|
|
1679
|
+
content: text
|
|
1680
|
+
});
|
|
1681
|
+
const head = text.slice(0, Math.floor(max * .6));
|
|
1682
|
+
const tail = text.slice(-Math.floor(max * .3));
|
|
1683
|
+
return `${head}\n...[truncated ${text.length - head.length - tail.length} chars; full tool result saved as artifact "${artifact.id}"]...\n${tail}`;
|
|
1684
|
+
}
|
|
1685
|
+
/** SkillStateGuard 判定(无注入默认全放行;仅显式 false 拦截) */
|
|
1686
|
+
async #guardDenied(kind, skillName) {
|
|
1687
|
+
const check = this.#deps.skillStateGuard?.[kind];
|
|
1688
|
+
if (!check) return false;
|
|
1689
|
+
return await check(skillName) === false;
|
|
1617
1690
|
}
|
|
1618
1691
|
/** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
|
|
1619
1692
|
async #activateSkill(skillName, state, via, skillMdText) {
|
|
1693
|
+
if (await this.#guardDenied("canActivate", skillName)) {
|
|
1694
|
+
state.trace.record("run.warning", { message: `Skill "${skillName}" is blocked by the skill state guard (activate)` });
|
|
1695
|
+
return "";
|
|
1696
|
+
}
|
|
1620
1697
|
state.activated.add(skillName);
|
|
1621
1698
|
state.trace.record("skill.activated", { data: {
|
|
1622
1699
|
skillName,
|
|
@@ -1635,7 +1712,9 @@ var AgentLoop = class {
|
|
|
1635
1712
|
const rawAllowed = metadata["allowed-tools"];
|
|
1636
1713
|
if (rawAllowed !== void 0) if (Array.isArray(rawAllowed)) allowedTools = rawAllowed.filter((e) => typeof e === "string");
|
|
1637
1714
|
else state.trace.record("run.warning", { message: `Skill "${skillName}" has a non-array "allowed-tools" metadata entry; ignored` });
|
|
1638
|
-
} catch {
|
|
1715
|
+
} catch (e) {
|
|
1716
|
+
state.trace.record("run.warning", { message: `Failed to read or parse SKILL.md of skill "${skillName}": ${messageOf$1(e)}` });
|
|
1717
|
+
}
|
|
1639
1718
|
const executor = this.#deps.executor;
|
|
1640
1719
|
const loaded = [];
|
|
1641
1720
|
if (executor) {
|
|
@@ -1654,7 +1733,9 @@ var AgentLoop = class {
|
|
|
1654
1733
|
await this.#enrichDefinition(root, match[1], def, state);
|
|
1655
1734
|
state.activatedTools.set(def.name, def);
|
|
1656
1735
|
loaded.push(def.name);
|
|
1657
|
-
} catch {
|
|
1736
|
+
} catch (e) {
|
|
1737
|
+
state.trace.record("run.warning", { message: `Failed to load definition of script "${match[1]}" for skill "${skillName}": ${messageOf$1(e)}` });
|
|
1738
|
+
}
|
|
1658
1739
|
}
|
|
1659
1740
|
}
|
|
1660
1741
|
let note = loaded.length ? `\n\nActivated tools: ${loaded.join(", ")}` : "";
|
|
@@ -1728,12 +1809,14 @@ var AgentLoop = class {
|
|
|
1728
1809
|
onArtifactCreated: (artifact) => createdIds.add(artifact.id)
|
|
1729
1810
|
});
|
|
1730
1811
|
try {
|
|
1812
|
+
if (await this.#guardDenied("canExecute", skillName)) return toolError("SKILL_DISABLED", `Skill "${skillName}" is blocked by the skill state guard (execute)`);
|
|
1813
|
+
const remainingMs = state.totalTimeoutMs - this.#elapsed(state);
|
|
1731
1814
|
const result = await this.#deps.executor.execute({
|
|
1732
1815
|
skillRoot: root,
|
|
1733
1816
|
scriptName,
|
|
1734
1817
|
args,
|
|
1735
1818
|
context,
|
|
1736
|
-
timeoutMs: state.toolTimeoutMs
|
|
1819
|
+
timeoutMs: Math.max(1, Math.min(state.toolTimeoutMs, remainingMs))
|
|
1737
1820
|
});
|
|
1738
1821
|
if (createdIds.size > 0) {
|
|
1739
1822
|
const fresh = (await this.#deps.artifactStore.listArtifacts(state.runId)).filter((a) => createdIds.has(a.id));
|
|
@@ -1912,6 +1995,13 @@ var WebSkillRuntime = class {
|
|
|
1912
1995
|
get events() {
|
|
1913
1996
|
return this.#events;
|
|
1914
1997
|
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Catalog 缓存失效:下次 run/discover 重新扫描技能目录。
|
|
2000
|
+
* 与 SkillManager onChanged 接线:`new SkillManager({ ..., onChanged: () => runtime.invalidate() })`。
|
|
2001
|
+
*/
|
|
2002
|
+
invalidate() {
|
|
2003
|
+
this.#catalogCache = void 0;
|
|
2004
|
+
}
|
|
1915
2005
|
async discover() {
|
|
1916
2006
|
const result = await this.#discovery.discover();
|
|
1917
2007
|
this.#catalogCache = {
|
|
@@ -1924,10 +2014,12 @@ var WebSkillRuntime = class {
|
|
|
1924
2014
|
if (!this.#catalogCache) await this.discover();
|
|
1925
2015
|
const cache = this.#catalogCache;
|
|
1926
2016
|
if (!cache) throw new Error("discover() did not populate the catalog cache");
|
|
2017
|
+
const providerFailures = [];
|
|
1927
2018
|
const providerEntries = (await Promise.all((this.#deps.skillProviders ?? []).map(async (p) => {
|
|
1928
2019
|
try {
|
|
1929
2020
|
return await p.listSkills();
|
|
1930
|
-
} catch {
|
|
2021
|
+
} catch (e) {
|
|
2022
|
+
providerFailures.push(e instanceof Error ? e.message : String(e));
|
|
1931
2023
|
return [];
|
|
1932
2024
|
}
|
|
1933
2025
|
}))).flat();
|
|
@@ -1951,12 +2043,20 @@ var WebSkillRuntime = class {
|
|
|
1951
2043
|
externalTools: this.#deps.externalTools,
|
|
1952
2044
|
skillProviders: this.#deps.skillProviders,
|
|
1953
2045
|
catalogFilter: this.#deps.catalogFilter,
|
|
1954
|
-
snapshotStore: this.#deps.snapshotStore
|
|
2046
|
+
snapshotStore: this.#deps.snapshotStore,
|
|
2047
|
+
skillStateGuard: this.#deps.skillStateGuard
|
|
1955
2048
|
}, this.#deps.config).run({
|
|
1956
2049
|
sessionId: this.#session.id,
|
|
1957
2050
|
userPrompt,
|
|
1958
2051
|
route
|
|
1959
2052
|
});
|
|
2053
|
+
for (const failure of providerFailures) result.run.trace.push({
|
|
2054
|
+
id: `evt-provider-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
2055
|
+
runId: result.run.id,
|
|
2056
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2057
|
+
type: "run.warning",
|
|
2058
|
+
message: `Skill provider listSkills() failed: ${failure}`
|
|
2059
|
+
});
|
|
1960
2060
|
if (this.#deps.onSkillMiss && result.run.activeSkillNames.length === 0) this.#deps.onSkillMiss({
|
|
1961
2061
|
prompt: userPrompt,
|
|
1962
2062
|
run: result.run
|
|
@@ -2040,7 +2140,8 @@ var WebSkillRuntime = class {
|
|
|
2040
2140
|
externalTools: this.#deps.externalTools,
|
|
2041
2141
|
skillProviders: this.#deps.skillProviders,
|
|
2042
2142
|
catalogFilter: this.#deps.catalogFilter,
|
|
2043
|
-
snapshotStore: store
|
|
2143
|
+
snapshotStore: store,
|
|
2144
|
+
skillStateGuard: this.#deps.skillStateGuard
|
|
2044
2145
|
}, this.#deps.config).resume(snapshot);
|
|
2045
2146
|
}
|
|
2046
2147
|
};
|
|
@@ -2279,6 +2380,7 @@ var FsArtifactStore = class {
|
|
|
2279
2380
|
});
|
|
2280
2381
|
}
|
|
2281
2382
|
async listArtifacts(runId) {
|
|
2383
|
+
assertSafePathSegment(runId, "runId");
|
|
2282
2384
|
const indexPath = `${this.#root}/${runId}/${INDEX_FILE}`;
|
|
2283
2385
|
if (!await this.#fs.exists(indexPath)) return [];
|
|
2284
2386
|
const raw = await this.#fs.readText(indexPath);
|
|
@@ -2290,6 +2392,7 @@ var FsArtifactStore = class {
|
|
|
2290
2392
|
}
|
|
2291
2393
|
}
|
|
2292
2394
|
#artifactPath(runId, artifactPath) {
|
|
2395
|
+
assertSafePathSegment(runId, "runId");
|
|
2293
2396
|
return resolveInsideRoot(`${this.#root}/${runId}`, artifactPath);
|
|
2294
2397
|
}
|
|
2295
2398
|
async #persist(input) {
|