@webskill/sdk 0.1.4 → 0.2.0

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.
@@ -0,0 +1,143 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ //#region ../node/dist/env--jJB-TSX.js
4
+ /** 解析 .env(简单 KEY=VALUE 行解析,不引依赖);文件缺失返回 undefined */
5
+ function readEnvVars(dotenvPath) {
6
+ let raw;
7
+ try {
8
+ raw = readFileSync(dotenvPath, "utf8");
9
+ } catch {
10
+ return;
11
+ }
12
+ const vars = /* @__PURE__ */ new Map();
13
+ for (const line of raw.split(/\r?\n/)) {
14
+ const trimmed = line.trim();
15
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
16
+ const eq = trimmed.indexOf("=");
17
+ if (eq <= 0) continue;
18
+ const key = trimmed.slice(0, eq).trim();
19
+ let value = trimmed.slice(eq + 1).trim();
20
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
21
+ vars.set(key, value);
22
+ }
23
+ return vars;
24
+ }
25
+ /**
26
+ * 读取 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL(优先),
27
+ * 向后兼容 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
28
+ */
29
+ function loadLlmConfigFromEnv(dotenvPath = ".env") {
30
+ const vars = readEnvVars(dotenvPath);
31
+ if (!vars) return void 0;
32
+ const baseUrl = vars.get("LLM_BASE_URL") ?? vars.get("VITE_BASE_URL");
33
+ const apiKey = vars.get("LLM_API_KEY") ?? vars.get("VITE_API_KEY");
34
+ const model = vars.get("LLM_MODEL") ?? vars.get("VITE_MODEL_NAME");
35
+ if (!baseUrl || !apiKey || !model) return void 0;
36
+ return {
37
+ baseUrl,
38
+ apiKey,
39
+ model
40
+ };
41
+ }
42
+ /** ANTHROPIC_API_KEY / ANTHROPIC_MODEL(可选 ANTHROPIC_BASE_URL);缺项返回 undefined */
43
+ function loadAnthropicConfigFromEnv(dotenvPath = ".env") {
44
+ const vars = readEnvVars(dotenvPath);
45
+ if (!vars) return void 0;
46
+ const apiKey = vars.get("ANTHROPIC_API_KEY");
47
+ const model = vars.get("ANTHROPIC_MODEL");
48
+ if (!apiKey || !model) return void 0;
49
+ const baseUrl = vars.get("ANTHROPIC_BASE_URL");
50
+ return {
51
+ apiKey,
52
+ model,
53
+ ...baseUrl ? { baseUrl } : {}
54
+ };
55
+ }
56
+ /** GOOGLE_API_KEY / GOOGLE_MODEL(可选 GOOGLE_BASE_URL);缺项返回 undefined */
57
+ function loadGoogleConfigFromEnv(dotenvPath = ".env") {
58
+ const vars = readEnvVars(dotenvPath);
59
+ if (!vars) return void 0;
60
+ const apiKey = vars.get("GOOGLE_API_KEY");
61
+ const model = vars.get("GOOGLE_MODEL");
62
+ if (!apiKey || !model) return void 0;
63
+ const baseUrl = vars.get("GOOGLE_BASE_URL");
64
+ return {
65
+ apiKey,
66
+ model,
67
+ ...baseUrl ? { baseUrl } : {}
68
+ };
69
+ }
70
+ let capabilitiesCache;
71
+ /**
72
+ * 探测 LLM 能力(结果模块级缓存):
73
+ * - available:GET /models 可达(与 checkAvailability 同语义)
74
+ * - nonStreaming:最小非流式 chat completion(max_tokens=1,短超时);
75
+ * HTTP 400/404 或明确不支持非流式的错误 → false
76
+ * - streaming 能力由 available 推断(SSE 解析在 SDK 侧实现)
77
+ * 环境变量 VITE_LLM_NON_STREAMING=0 可显式强制 nonStreaming=false(模拟流式-only 回归)。
78
+ */
79
+ function probeLlmCapabilities(config) {
80
+ capabilitiesCache ??= doProbeLlmCapabilities(config);
81
+ return capabilitiesCache;
82
+ }
83
+ async function doProbeLlmCapabilities(config) {
84
+ const baseUrl = config.baseUrl.replace(/\/+$/, "");
85
+ const headers = { authorization: `Bearer ${config.apiKey}` };
86
+ let available;
87
+ try {
88
+ available = (await fetch(`${baseUrl}/models`, {
89
+ headers,
90
+ signal: AbortSignal.timeout(1e4)
91
+ })).ok;
92
+ } catch {
93
+ available = false;
94
+ }
95
+ if (!available) return {
96
+ available: false,
97
+ nonStreaming: false,
98
+ reason: "endpoint unreachable or unauthorized"
99
+ };
100
+ if (process.env["VITE_LLM_NON_STREAMING"] === "0") return {
101
+ available: true,
102
+ nonStreaming: false,
103
+ reason: "non-streaming disabled via VITE_LLM_NON_STREAMING=0 (simulated)"
104
+ };
105
+ try {
106
+ const res = await fetch(`${baseUrl}/chat/completions`, {
107
+ method: "POST",
108
+ headers: {
109
+ ...headers,
110
+ "content-type": "application/json"
111
+ },
112
+ body: JSON.stringify({
113
+ model: config.model,
114
+ messages: [{
115
+ role: "user",
116
+ content: "OK"
117
+ }],
118
+ max_tokens: 1
119
+ }),
120
+ signal: AbortSignal.timeout(15e3)
121
+ });
122
+ const contentType = res.headers.get("content-type") ?? "";
123
+ if (res.ok && !contentType.includes("text/event-stream")) return {
124
+ available: true,
125
+ nonStreaming: true
126
+ };
127
+ const detail = (await res.text().catch(() => "")).slice(0, 200);
128
+ return {
129
+ available: true,
130
+ nonStreaming: false,
131
+ reason: `endpoint rejects non-streaming chat completions (HTTP ${res.status})${detail ? `: ${detail}` : ""}`
132
+ };
133
+ } catch (e) {
134
+ return {
135
+ available: true,
136
+ nonStreaming: false,
137
+ reason: `non-streaming probe failed: ${e instanceof Error ? e.message : String(e)}`
138
+ };
139
+ }
140
+ }
141
+
142
+ //#endregion
143
+ export { probeLlmCapabilities as i, loadGoogleConfigFromEnv as n, loadLlmConfigFromEnv as r, loadAnthropicConfigFromEnv as t };
@@ -0,0 +1,38 @@
1
+ //#region ../node/dist/env-BPUBZCwJ.d.ts
2
+ //#region src/env.d.ts
3
+ interface LlmEnvConfig {
4
+ baseUrl: string;
5
+ apiKey: string;
6
+ model: string;
7
+ }
8
+ /** per-provider env 配置(baseUrl 可选,客户端自带默认) */
9
+ interface ProviderEnvConfig {
10
+ apiKey: string;
11
+ model: string;
12
+ baseUrl?: string;
13
+ }
14
+ /**
15
+ * 读取 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL(优先),
16
+ * 向后兼容 VITE_BASE_URL / VITE_API_KEY / VITE_MODEL_NAME;缺项返回 undefined 供测试 skip 判断。
17
+ */
18
+ declare function loadLlmConfigFromEnv(dotenvPath?: string): LlmEnvConfig | undefined;
19
+ /** ANTHROPIC_API_KEY / ANTHROPIC_MODEL(可选 ANTHROPIC_BASE_URL);缺项返回 undefined */
20
+ declare function loadAnthropicConfigFromEnv(dotenvPath?: string): ProviderEnvConfig | undefined;
21
+ /** GOOGLE_API_KEY / GOOGLE_MODEL(可选 GOOGLE_BASE_URL);缺项返回 undefined */
22
+ declare function loadGoogleConfigFromEnv(dotenvPath?: string): ProviderEnvConfig | undefined;
23
+ interface LlmCapabilities {
24
+ available: boolean;
25
+ nonStreaming: boolean;
26
+ reason?: string;
27
+ }
28
+ /**
29
+ * 探测 LLM 能力(结果模块级缓存):
30
+ * - available:GET /models 可达(与 checkAvailability 同语义)
31
+ * - nonStreaming:最小非流式 chat completion(max_tokens=1,短超时);
32
+ * HTTP 400/404 或明确不支持非流式的错误 → false
33
+ * - streaming 能力由 available 推断(SSE 解析在 SDK 侧实现)
34
+ * 环境变量 VITE_LLM_NON_STREAMING=0 可显式强制 nonStreaming=false(模拟流式-only 回归)。
35
+ */
36
+ declare function probeLlmCapabilities(config: LlmEnvConfig): Promise<LlmCapabilities>;
37
+ //#endregion
38
+ export { loadGoogleConfigFromEnv as a, loadAnthropicConfigFromEnv as i, LlmEnvConfig as n, loadLlmConfigFromEnv as o, ProviderEnvConfig as r, probeLlmCapabilities as s, LlmCapabilities as t };
@@ -1,6 +1,6 @@
1
- import { L as SkillManifest, N as SkillDocument, S as FileSystemProvider, c as LlmClient, j as SkillCatalogEntry, u as LlmMessage, v as UiBridge } from "./types-CgNC-oQu-DYrxhD3z.js";
2
- import { ct as WebSkillRuntime, q as RuntimeRun } from "./index-BSS3EWY-.js";
3
- import { f as SkillManager } from "./index-npFMt2Zw.js";
1
+ import { F as SkillDocument, N as SkillCatalogEntry, c as LlmClient, u as LlmMessage, v as UiBridge, w as FileSystemProvider, z as SkillManifest } from "./types-CKm5G_eQ-8W8FnP4u.js";
2
+ import { dt as WebSkillRuntime, q as RuntimeRun, tt as SkillStateGuard } from "./index-CySxIvRz.js";
3
+ import { l as SkillManager } from "./index-Vn63HJRO.js";
4
4
  //#region ../governance/dist/index.d.ts
5
5
  //#region src/types.d.ts
6
6
  type CandidateStatus = 'draft' | 'pending-review' | 'approved' | 'published' | 'rejected';
@@ -32,6 +32,10 @@ interface AuditEvent {
32
32
  actor?: string;
33
33
  ts: string;
34
34
  data?: Record<string, unknown>;
35
+ /** hash 链:上一条事件的 hash(首条为 GENESIS) */
36
+ prevHash?: string;
37
+ /** 本事件规范化载荷 + prevHash 的 sha256 */
38
+ hash?: string;
35
39
  }
36
40
  interface AuditLog {
37
41
  append(event: Omit<AuditEvent, 'id' | 'ts'> & {
@@ -51,6 +55,8 @@ interface SkillVersion {
51
55
  reason: string;
52
56
  manifest: SkillManifest;
53
57
  auditEventId?: string;
58
+ /** 发布时捕获的技能归档(zip 相对路径,applyRollback 恢复用;0.2.0 起记录) */
59
+ archivePath?: string;
54
60
  }
55
61
  type SkillState = 'active' | 'quarantined' | 'deprecated' | 'disabled';
56
62
  //#endregion
@@ -151,7 +157,10 @@ declare class SkillVersionStore {
151
157
  reason: string;
152
158
  manifest: SkillManifest;
153
159
  parentVersionId?: string;
160
+ archive?: Uint8Array;
154
161
  }): Promise<SkillVersion>;
162
+ /** 读取版本归档字节(applyRollback 用;未捕获归档的旧版本 → GOVERNANCE_FAILED) */
163
+ readArchive(skillName: string, versionId: string): Promise<Uint8Array>;
155
164
  list(skillName: string): Promise<SkillVersion[]>;
156
165
  get(skillName: string, versionId: string): Promise<SkillVersion>;
157
166
  /** 回滚:基于旧 manifest 追加新版本 + skill.rolled_back 审计 */
@@ -182,10 +191,25 @@ declare class ApprovalWorkflow {
182
191
  publish(candidateId: string, input: {
183
192
  actor: string;
184
193
  }): Promise<SkillManifest>;
194
+ /**
195
+ * 真实回滚(受审批保护:仅经显式 actor 调用并全程审计):
196
+ * 版本归档解包 → staging 校验 → 原子安装(复用安装管线 swap)→ 追加新版本 + 审计。
197
+ * RepairPlanner 的 rollback 选项(targetVersionId)经本方法执行。
198
+ */
199
+ applyRollback(skillName: string, versionId: string, input: {
200
+ actor: string;
201
+ reason?: string;
202
+ }): Promise<SkillManifest>;
185
203
  }
186
204
  //#endregion
187
205
  //#region src/audit/fsAuditLog.d.ts
188
- /** JSONL 追加的审计日志:<managedRoot>/.webskill/audit.jsonl;跨实例可恢复查询 */
206
+ interface AuditChainVerification {
207
+ ok: boolean;
208
+ /** 首个断链位置(行号,0 起;ok 时 undefined) */
209
+ brokenAt?: number;
210
+ reason?: string;
211
+ }
212
+ /** JSONL 追加的审计日志:<managedRoot>/.webskill/audit.jsonl;跨实例可恢复查询;prevHash 链可校验完整性 */
189
213
  declare class FsAuditLog implements AuditLog {
190
214
  #private;
191
215
  constructor(deps: {
@@ -194,7 +218,7 @@ declare class FsAuditLog implements AuditLog {
194
218
  now?: () => string;
195
219
  createId?: () => string;
196
220
  });
197
- append(event: Omit<AuditEvent, 'id' | 'ts'> & {
221
+ append(event: Omit<AuditEvent, 'id' | 'ts' | 'prevHash' | 'hash'> & {
198
222
  id?: string;
199
223
  ts?: string;
200
224
  }): Promise<AuditEvent>;
@@ -203,6 +227,8 @@ declare class FsAuditLog implements AuditLog {
203
227
  type?: string;
204
228
  since?: string;
205
229
  }): Promise<AuditEvent[]>;
230
+ /** hash 链完整性校验:逐行重算 hash 并核对 prevHash 链接 */
231
+ verifyChain(): Promise<AuditChainVerification>;
206
232
  }
207
233
  //#endregion
208
234
  //#region src/repair/failureAnalyzer.d.ts
@@ -234,7 +260,7 @@ interface RepairOption {
234
260
  }
235
261
  /**
236
262
  * 修订选项规划:patch(按诊断给具体文件变更)/ rollback(有旧版本时)/ quarantine。
237
- * 注意:任何 option 的 apply 都必须经 ApprovalWorkflow 审批后执行。
263
+ * 注意:任何 option 的 apply 都必须经 ApprovalWorkflow 审批后执行;rollback 选项(targetVersionId)经 ApprovalWorkflow.applyRollback 真实恢复文件。
238
264
  */
239
265
  declare class RepairPlanner {
240
266
  plan(input: {
@@ -270,6 +296,12 @@ declare class SkillStatePolicy {
270
296
  }): Promise<SkillState>;
271
297
  canRoute(state: SkillState): boolean;
272
298
  canExecute(state: SkillState): boolean;
299
+ /**
300
+ * runtime SkillStateGuard 适配(0.1.5 一致拦截):
301
+ * canRead:active/deprecated 放行;canActivate/canExecute 与 canRoute/canExecute 语义对齐;
302
+ * quarantined/disabled 三入口全拦截。
303
+ */
304
+ toSkillStateGuard(): SkillStateGuard;
273
305
  /** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
274
306
  assertExecutable(skillName: string): Promise<void>;
275
307
  /** runtime catalogFilter 实现:quarantined/deprecated/disabled 技能被路由过滤 */
@@ -1,8 +1,8 @@
1
- import { E as validateSkills, T as resolveInsideRoot, l as WebSkillError, u as assertSafePathSegment, v as isValidSkillName } from "./dist-DvoBsChX.js";
2
- import { i as NodeFS } from "./dist-BVXZF7H_.js";
1
+ import { A as unzipWithLimits, b as isValidSkillName, d as assertSafePathSegment, j as validateSkills, k as resolveInsideRoot, u as WebSkillError } from "./dist-D0saNPi_.js";
2
+ import { i as NodeFS, l as exportArchive } from "./dist-Dj6QjHBn.js";
3
3
  import path from "node:path";
4
- import { mkdtemp } from "node:fs/promises";
5
4
  import { tmpdir } from "node:os";
5
+ import { mkdtemp } from "node:fs/promises";
6
6
  import { createHash } from "node:crypto";
7
7
 
8
8
  //#region ../governance/dist/index.js
@@ -307,9 +307,15 @@ var ApprovalWorkflow = class {
307
307
  type: "local",
308
308
  path: skillDir
309
309
  });
310
+ const archiveOut = `${stagingRoot}/version-archive.zip`;
311
+ await exportArchive(this.#fs, `${this.#skillManager.managedRoot}/${candidate.name}`, {
312
+ format: "zip",
313
+ outPath: archiveOut
314
+ });
310
315
  await this.#versions.add(candidate.name, {
311
316
  reason: `Publish candidate ${candidateId}`,
312
- manifest
317
+ manifest,
318
+ archive: await this.#fs.readBinary(archiveOut)
313
319
  });
314
320
  await this.#store.updateStatus(candidateId, "published");
315
321
  await this.#audit.append({
@@ -325,11 +331,79 @@ var ApprovalWorkflow = class {
325
331
  } catch (e) {
326
332
  if (e instanceof WebSkillError) throw e;
327
333
  throw new WebSkillError("GOVERNANCE_FAILED", `Failed to publish candidate "${candidateId}": ${messageOf$1(e)}`, e);
334
+ } finally {
335
+ try {
336
+ await this.#fs.remove(stagingRoot, { recursive: true });
337
+ } catch {}
338
+ }
339
+ }
340
+ /**
341
+ * 真实回滚(受审批保护:仅经显式 actor 调用并全程审计):
342
+ * 版本归档解包 → staging 校验 → 原子安装(复用安装管线 swap)→ 追加新版本 + 审计。
343
+ * RepairPlanner 的 rollback 选项(targetVersionId)经本方法执行。
344
+ */
345
+ async applyRollback(skillName, versionId, input) {
346
+ assertSafePathSegment(skillName, "skill name");
347
+ assertSafePathSegment(versionId, "version id");
348
+ const version = await this.#versions.get(skillName, versionId);
349
+ const archive = await this.#versions.readArchive(skillName, versionId);
350
+ const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-rollback-"))).split(path.sep).join("/");
351
+ try {
352
+ const skillDir = `${stagingRoot}/${skillName}`;
353
+ for (const [rel, content] of await unzipWithLimits(archive)) {
354
+ if (rel.endsWith("/")) continue;
355
+ await this.#fs.writeBinary(resolveInsideRoot(skillDir, rel), content);
356
+ }
357
+ const report = await validateSkills(this.#fs, [stagingRoot]);
358
+ if (!report.ok) {
359
+ const errors = report.issues.filter((i) => i.severity === "error");
360
+ throw new WebSkillError("GOVERNANCE_FAILED", `Rollback archive of "${skillName}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
361
+ }
362
+ const manifest = await this.#skillManager.install({
363
+ type: "local",
364
+ path: skillDir
365
+ });
366
+ if (manifest.integrity.digest !== version.manifest.integrity.digest) throw new WebSkillError("GOVERNANCE_FAILED", `Rollback of "${skillName}" to version "${versionId}" produced a digest mismatch: expected ${version.manifest.integrity.digest}, got ${manifest.integrity.digest}`);
367
+ await this.#versions.add(skillName, {
368
+ reason: input.reason ?? `Rollback to version ${versionId}`,
369
+ manifest,
370
+ archive
371
+ });
372
+ await this.#audit.append({
373
+ type: "skill.rolled_back",
374
+ target: skillName,
375
+ actor: input.actor,
376
+ data: {
377
+ targetVersionId: versionId,
378
+ reason: input.reason
379
+ }
380
+ });
381
+ return manifest;
382
+ } catch (e) {
383
+ if (e instanceof WebSkillError) throw e;
384
+ throw new WebSkillError("GOVERNANCE_FAILED", `Failed to roll back "${skillName}" to version "${versionId}": ${messageOf$1(e)}`, e);
385
+ } finally {
386
+ try {
387
+ await this.#fs.remove(stagingRoot, { recursive: true });
388
+ } catch {}
328
389
  }
329
390
  }
330
391
  };
331
392
  const fileOf$1 = (root) => `${root}/.webskill/audit.jsonl`;
332
- /** JSONL 追加的审计日志:<managedRoot>/.webskill/audit.jsonl;跨实例可恢复查询 */
393
+ const sha256Hex = (text) => createHash("sha256").update(text, "utf8").digest("hex");
394
+ /** 链字段之外的规范化事件载荷(hash 计算输入) */
395
+ function canonical(event) {
396
+ return JSON.stringify({
397
+ id: event.id,
398
+ ts: event.ts,
399
+ type: event.type,
400
+ target: event.target,
401
+ actor: event.actor,
402
+ data: event.data,
403
+ prevHash: event.prevHash
404
+ });
405
+ }
406
+ /** JSONL 追加的审计日志:<managedRoot>/.webskill/audit.jsonl;跨实例可恢复查询;prevHash 链可校验完整性 */
333
407
  var FsAuditLog = class {
334
408
  #root;
335
409
  #fs;
@@ -342,16 +416,26 @@ var FsAuditLog = class {
342
416
  this.#createId = deps.createId;
343
417
  }
344
418
  async append(event) {
419
+ const path = fileOf$1(this.#root);
420
+ const existing = await this.#fs.exists(path) ? await this.#fs.readText(path) : "";
421
+ const lines = existing.split("\n").filter((l) => l.trim() !== "");
422
+ let prevHash = "GENESIS";
423
+ if (lines.length > 0) try {
424
+ const last = JSON.parse(lines.at(-1));
425
+ prevHash = last.hash ?? sha256Hex(canonical(last));
426
+ } catch {
427
+ prevHash = "GENESIS";
428
+ }
345
429
  const full = {
346
430
  id: event.id ?? this.#createId?.() ?? `audit-${Math.random().toString(36).slice(2, 10)}`,
347
431
  ts: event.ts ?? this.#now?.() ?? (/* @__PURE__ */ new Date()).toISOString(),
348
432
  type: event.type,
349
433
  target: event.target,
350
434
  ...event.actor !== void 0 ? { actor: event.actor } : {},
351
- ...event.data !== void 0 ? { data: event.data } : {}
435
+ ...event.data !== void 0 ? { data: event.data } : {},
436
+ prevHash
352
437
  };
353
- const path = fileOf$1(this.#root);
354
- const existing = await this.#fs.exists(path) ? await this.#fs.readText(path) : "";
438
+ full.hash = sha256Hex(canonical(full));
355
439
  const prefix = existing === "" || existing.endsWith("\n") ? existing : `${existing}\n`;
356
440
  await this.#fs.writeText(path, `${prefix}${JSON.stringify(full)}\n`);
357
441
  return full;
@@ -371,6 +455,38 @@ var FsAuditLog = class {
371
455
  }
372
456
  return events;
373
457
  }
458
+ /** hash 链完整性校验:逐行重算 hash 并核对 prevHash 链接 */
459
+ async verifyChain() {
460
+ const path = fileOf$1(this.#root);
461
+ if (!await this.#fs.exists(path)) return { ok: true };
462
+ const lines = (await this.#fs.readText(path)).split("\n").filter((l) => l.trim() !== "");
463
+ let expectedPrev = "GENESIS";
464
+ for (let i = 0; i < lines.length; i++) {
465
+ let event;
466
+ try {
467
+ event = JSON.parse(lines[i]);
468
+ } catch {
469
+ return {
470
+ ok: false,
471
+ brokenAt: i,
472
+ reason: "line is not valid JSON"
473
+ };
474
+ }
475
+ if (event.prevHash !== expectedPrev) return {
476
+ ok: false,
477
+ brokenAt: i,
478
+ reason: "prevHash link mismatch (events may have been removed or reordered)"
479
+ };
480
+ const expectedHash = sha256Hex(canonical(event));
481
+ if (event.hash !== expectedHash) return {
482
+ ok: false,
483
+ brokenAt: i,
484
+ reason: "event hash mismatch (event content was tampered)"
485
+ };
486
+ expectedPrev = event.hash;
487
+ }
488
+ return { ok: true };
489
+ }
374
490
  };
375
491
  const dirOf = (root, skillName) => {
376
492
  assertSafePathSegment(skillName, "skill name");
@@ -400,9 +516,20 @@ var SkillVersionStore = class {
400
516
  manifest: input.manifest
401
517
  };
402
518
  assertSafePathSegment(version.versionId, "version id");
519
+ if (input.archive) {
520
+ const archivePath = `${dirOf(this.#root, skillName)}/${version.versionId}.zip`;
521
+ await this.#fs.writeBinary(archivePath, input.archive);
522
+ version.archivePath = archivePath;
523
+ }
403
524
  await this.#fs.writeText(`${dirOf(this.#root, skillName)}/${version.versionId}.json`, JSON.stringify(version, null, 2));
404
525
  return version;
405
526
  }
527
+ /** 读取版本归档字节(applyRollback 用;未捕获归档的旧版本 → GOVERNANCE_FAILED) */
528
+ async readArchive(skillName, versionId) {
529
+ const version = await this.get(skillName, versionId);
530
+ if (!version.archivePath || !await this.#fs.exists(version.archivePath)) throw new WebSkillError("GOVERNANCE_FAILED", `Version "${versionId}" of skill "${skillName}" has no captured archive (recorded before archive capture existed)`);
531
+ return this.#fs.readBinary(version.archivePath);
532
+ }
406
533
  async list(skillName) {
407
534
  const dir = dirOf(this.#root, skillName);
408
535
  if (!await this.#fs.exists(dir)) return [];
@@ -484,7 +611,7 @@ var FailureAnalyzer = class {
484
611
  };
485
612
  /**
486
613
  * 修订选项规划:patch(按诊断给具体文件变更)/ rollback(有旧版本时)/ quarantine。
487
- * 注意:任何 option 的 apply 都必须经 ApprovalWorkflow 审批后执行。
614
+ * 注意:任何 option 的 apply 都必须经 ApprovalWorkflow 审批后执行;rollback 选项(targetVersionId)经 ApprovalWorkflow.applyRollback 真实恢复文件。
488
615
  */
489
616
  var RepairPlanner = class {
490
617
  plan(input) {
@@ -522,6 +649,8 @@ var SkillStatePolicy = class {
522
649
  #audit;
523
650
  #failureThreshold;
524
651
  #loaded;
652
+ /** 缓存文件 mtime(states.json 外部变更时按 mtime 重载) */
653
+ #loadedMtimeMs;
525
654
  constructor(deps) {
526
655
  this.#root = deps.root.replace(/\/+$/, "");
527
656
  this.#fs = deps.fs;
@@ -529,17 +658,33 @@ var SkillStatePolicy = class {
529
658
  this.#failureThreshold = deps.failureThreshold ?? 3;
530
659
  }
531
660
  async #load() {
532
- if (this.#loaded) return this.#loaded;
533
661
  const path = fileOf(this.#root);
662
+ if (this.#loaded) try {
663
+ const stat = await this.#fs.stat(path);
664
+ if (stat.mtimeMs !== void 0 && stat.mtimeMs === this.#loadedMtimeMs) return this.#loaded;
665
+ } catch {
666
+ return this.#loaded;
667
+ }
534
668
  this.#loaded = await this.#fs.exists(path) ? JSON.parse(await this.#fs.readText(path)) : {
535
669
  states: {},
536
670
  failures: {}
537
671
  };
672
+ try {
673
+ this.#loadedMtimeMs = (await this.#fs.stat(path)).mtimeMs;
674
+ } catch {
675
+ this.#loadedMtimeMs = void 0;
676
+ }
538
677
  return this.#loaded;
539
678
  }
540
679
  async #persist() {
541
680
  const data = await this.#load();
542
- await this.#fs.writeText(fileOf(this.#root), JSON.stringify(data, null, 2));
681
+ const path = fileOf(this.#root);
682
+ await this.#fs.writeText(path, JSON.stringify(data, null, 2));
683
+ try {
684
+ this.#loadedMtimeMs = (await this.#fs.stat(path)).mtimeMs;
685
+ } catch {
686
+ this.#loadedMtimeMs = void 0;
687
+ }
543
688
  }
544
689
  async getState(skillName) {
545
690
  assertSafePathSegment(skillName, "skill name");
@@ -590,6 +735,21 @@ var SkillStatePolicy = class {
590
735
  canExecute(state) {
591
736
  return state === "active" || state === "deprecated";
592
737
  }
738
+ /**
739
+ * runtime SkillStateGuard 适配(0.1.5 一致拦截):
740
+ * canRead:active/deprecated 放行;canActivate/canExecute 与 canRoute/canExecute 语义对齐;
741
+ * quarantined/disabled 三入口全拦截。
742
+ */
743
+ toSkillStateGuard() {
744
+ return {
745
+ canRead: async (skillName) => {
746
+ const state = await this.getState(skillName);
747
+ return state === "active" || state === "deprecated";
748
+ },
749
+ canActivate: async (skillName) => this.canRoute(await this.getState(skillName)),
750
+ canExecute: async (skillName) => this.canExecute(await this.getState(skillName))
751
+ };
752
+ }
593
753
  /** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
594
754
  async assertExecutable(skillName) {
595
755
  const state = await this.getState(skillName);