@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.
- package/dist/browser.d.ts +9 -4
- package/dist/browser.js +25 -22
- package/dist/{dist-MTc2KB03.js → dist-BJobG0i-.js} +30 -7
- package/dist/{dist-DAn07zHu.js → dist-BS5OpedX.js} +279 -63
- package/dist/{dist-DvoBsChX.js → dist-D0saNPi_.js} +154 -10
- package/dist/{dist-BVXZF7H_.js → dist-Dj6QjHBn.js} +148 -200
- package/dist/env--jJB-TSX-04klhTYi.js +143 -0
- package/dist/env-BPUBZCwJ-4jat_SVG.d.ts +38 -0
- package/dist/governance.d.ts +38 -6
- package/dist/governance.js +171 -11
- package/dist/{index-BSS3EWY-.d.ts → index-CySxIvRz.d.ts} +68 -3
- package/dist/{index-npFMt2Zw.d.ts → index-Vn63HJRO.d.ts} +15 -38
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +35 -9
- package/dist/node.d.ts +5 -4
- package/dist/node.js +5 -4
- package/dist/sandboxWorkerEntry.js +20 -3
- package/dist/{testing-S8SN9Lc6.js → testing-B4pq6JYa.js} +1 -1
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +3 -2
- package/dist/{types-CgNC-oQu-DYrxhD3z.d.ts → types-CKm5G_eQ-8W8FnP4u.d.ts} +41 -4
- package/dist/ui-react.d.ts +4 -2
- package/dist/ui-react.js +25 -4
- package/dist/ui-vue.d.ts +3 -2
- package/dist/ui-vue.js +44 -15
- package/dist/ui.d.ts +5 -3
- package/dist/ui.js +2 -2
- package/package.json +21 -9
|
@@ -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 };
|
package/dist/governance.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
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 技能被路由过滤 */
|
package/dist/governance.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as NodeFS } from "./dist-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|