@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 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, per-capability forced approval,
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 { H as SkillsLockfile, L as SkillManifest, P as SkillInstallSource, S as FileSystemProvider, W as VerifyResult, _ as RenderResultRequest, a as InteractionPolicy, c as LlmClient, d as LlmResponse, f as LlmStreamEvent, o as InteractionRequest, s as InteractionResponse, v as UiBridge, x as FileStat } from "./types-CgNC-oQu-Dq_A1yA-.js";
2
- import { $ as ToolDefinition, N as NetworkPolicy, X as ScriptExecutionContext, Z as ScriptExecutor, d as BridgeCapabilities, m as BridgeResponse, p as BridgeRequest, st as WebSkillApi, tt as ToolResult, u as ApprovalScope, ut as bridgeError, v as ExternalSkillProvider, xt as parseBridgeRequest, y as ExternalToolSource } from "./index-CqMuvcb2.js";
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 { E as verifyManifest, T as validateSkills, _ as isValidSkillName, b as parseSkillMarkdown, d as buildManifest, g as exportSkills, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, s as SkillDiscovery, u as buildCatalog, w as resolveInsideRoot, x as parseSkillPackManifest } from "./dist-DS1sfgHa.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-DZobLFh6.js";
3
- import { n as MockLlmClient } from "./testing-CbM6rJ-E.js";
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
- let entries;
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 = new Uint8Array(await res.arrayBuffer());
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
- targetDir = `${this.#managedRoot}/${name}`;
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
- await this.#upsertLockEntry(name, {
520
- digest: manifest.integrity.digest,
521
- installedAt: manifest.installedAt,
522
- source
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 targetDir = `${this.#managedRoot}/${entry.name}`;
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
- for (const manifest of manifests) await this.#upsertLockEntry(manifest.name, {
578
- digest: manifest.integrity.digest,
579
- installedAt: manifest.installedAt,
580
- source
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
- for (const file of manifest.files) {
621
- const path = `${this.#managedRoot}/${name}/${file.path}`;
622
- if (await this.#fs.exists(path)) actualHashes.set(file.path, await sha256HexWeb(await this.#fs.readBinary(path)));
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 { l as WebSkillError } from "./dist-DS1sfgHa.js";
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
- if (input.type === "authorize" && !response.cancelled) response.value = true;
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 { S as renderAvailableSkillsXml, T as validateSkills, b as parseSkillMarkdown, c as SkillReader, l as WebSkillError, s as SkillDiscovery, u as buildCatalog, w as resolveInsideRoot } from "./dist-DS1sfgHa.js";
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, startMs, 1, externalSpecs);
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, startMs, startTurn, externalSpecs) {
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 > this.#config.maxTurns) return finish("failed", "max-turns", `Agent loop exceeded the maximum of ${this.#config.maxTurns} turns`, "RUN_MAX_TURNS_EXCEEDED");
1134
- if (Date.parse(state.now()) - startMs > this.#config.totalTimeoutMs) return finish("failed", "timeout", `Agent loop exceeded the total timeout of ${this.#config.totalTimeoutMs}ms`, "RUN_TIMEOUT");
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: JSON.stringify(result)
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, startMs, snapshot.turn + 1, externalSpecs);
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
- return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}`);
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) {