@webskill/sdk 0.1.4 → 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/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-DYrxhD3z.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-BSS3EWY-.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 { D as verifyManifest, E as validateSkills, S as parseSkillPackManifest, T as resolveInsideRoot, _ as exportSkills, d as buildCatalog, f as buildManifest, l as WebSkillError, n as SKILLS_LOCKFILE, o as SKILL_PACK_FILE, r as SKILL_MANIFEST_FILE, s as SkillDiscovery, v as isValidSkillName, x as parseSkillMarkdown } from "./dist-DvoBsChX.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-DAn07zHu.js";
3
- import { n as MockLlmClient } from "./testing-S8SN9Lc6.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,
@@ -534,6 +531,7 @@ var BrowserSkillManager = class {
534
531
  throw e;
535
532
  }
536
533
  targetDir = void 0;
534
+ this.#onChanged?.();
537
535
  return manifest;
538
536
  } catch (e) {
539
537
  if (targetDir) await removeDirQuiet(fs, targetDir);
@@ -611,6 +609,7 @@ var BrowserSkillManager = class {
611
609
  installed.length = 0;
612
610
  throw e;
613
611
  }
612
+ this.#onChanged?.();
614
613
  return manifests;
615
614
  } catch (e) {
616
615
  for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
@@ -641,6 +640,7 @@ var BrowserSkillManager = class {
641
640
  try {
642
641
  await this.#fs.remove(targetDir, { recursive: true });
643
642
  await this.#removeLockEntry(name);
643
+ this.#onChanged?.();
644
644
  } catch (e) {
645
645
  throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf$1(e)}`, e);
646
646
  }
@@ -649,11 +649,9 @@ var BrowserSkillManager = class {
649
649
  if (!isValidSkillName(name)) throw new WebSkillError("INTEGRITY_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
650
650
  const manifest = await this.#readManifest(`${this.#managedRoot}/${name}`);
651
651
  const actualHashes = /* @__PURE__ */ new Map();
652
- for (const file of manifest.files) {
653
- const path = `${this.#managedRoot}/${name}/${file.path}`;
654
- if (await this.#fs.exists(path)) actualHashes.set(file.path, await sha256HexWeb(await this.#fs.readBinary(path)));
655
- }
656
- 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);
657
655
  }
658
656
  async listInstalled() {
659
657
  const path = lockfilePath(this.#managedRoot);
@@ -1,4 +1,4 @@
1
- import { l as WebSkillError } from "./dist-DvoBsChX.js";
1
+ import { u as WebSkillError } from "./dist-fZFZaf43.js";
2
2
 
3
3
  //#region ../ui/dist/index.js
4
4
  /** 提交值按请求类型归形(WebFormBridge 与框架组件库共享单一来源) */
@@ -1,4 +1,4 @@
1
- import { C as renderAvailableSkillsXml, E as validateSkills, T as resolveInsideRoot, c as SkillReader, d as buildCatalog, l as WebSkillError, s as SkillDiscovery, u as assertSafePathSegment, x as parseSkillMarkdown } from "./dist-DvoBsChX.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);
@@ -1618,8 +1665,35 @@ var AgentLoop = class {
1618
1665
  const detail = failures.length > 0 ? ` (provider errors: ${failures.join("; ")})` : "";
1619
1666
  return toolError("SKILL_NOT_FOUND", `Skill not found via external providers: ${skillKey}${detail}`);
1620
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;
1690
+ }
1621
1691
  /** 首次读到 SKILL.md 时激活技能:加载其 scripts 工具定义,供后续轮次使用;dependencies 级联激活(via 记录来源) */
1622
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
+ }
1623
1697
  state.activated.add(skillName);
1624
1698
  state.trace.record("skill.activated", { data: {
1625
1699
  skillName,
@@ -1735,12 +1809,14 @@ var AgentLoop = class {
1735
1809
  onArtifactCreated: (artifact) => createdIds.add(artifact.id)
1736
1810
  });
1737
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);
1738
1814
  const result = await this.#deps.executor.execute({
1739
1815
  skillRoot: root,
1740
1816
  scriptName,
1741
1817
  args,
1742
1818
  context,
1743
- timeoutMs: state.toolTimeoutMs
1819
+ timeoutMs: Math.max(1, Math.min(state.toolTimeoutMs, remainingMs))
1744
1820
  });
1745
1821
  if (createdIds.size > 0) {
1746
1822
  const fresh = (await this.#deps.artifactStore.listArtifacts(state.runId)).filter((a) => createdIds.has(a.id));
@@ -1919,6 +1995,13 @@ var WebSkillRuntime = class {
1919
1995
  get events() {
1920
1996
  return this.#events;
1921
1997
  }
1998
+ /**
1999
+ * Catalog 缓存失效:下次 run/discover 重新扫描技能目录。
2000
+ * 与 SkillManager onChanged 接线:`new SkillManager({ ..., onChanged: () => runtime.invalidate() })`。
2001
+ */
2002
+ invalidate() {
2003
+ this.#catalogCache = void 0;
2004
+ }
1922
2005
  async discover() {
1923
2006
  const result = await this.#discovery.discover();
1924
2007
  this.#catalogCache = {
@@ -1960,7 +2043,8 @@ var WebSkillRuntime = class {
1960
2043
  externalTools: this.#deps.externalTools,
1961
2044
  skillProviders: this.#deps.skillProviders,
1962
2045
  catalogFilter: this.#deps.catalogFilter,
1963
- snapshotStore: this.#deps.snapshotStore
2046
+ snapshotStore: this.#deps.snapshotStore,
2047
+ skillStateGuard: this.#deps.skillStateGuard
1964
2048
  }, this.#deps.config).run({
1965
2049
  sessionId: this.#session.id,
1966
2050
  userPrompt,
@@ -2056,7 +2140,8 @@ var WebSkillRuntime = class {
2056
2140
  externalTools: this.#deps.externalTools,
2057
2141
  skillProviders: this.#deps.skillProviders,
2058
2142
  catalogFilter: this.#deps.catalogFilter,
2059
- snapshotStore: store
2143
+ snapshotStore: store,
2144
+ skillStateGuard: this.#deps.skillStateGuard
2060
2145
  }, this.#deps.config).resume(snapshot);
2061
2146
  }
2062
2147
  };