@webskill/sdk 0.3.0 → 0.5.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/agent.d.ts +2 -0
- package/dist/agent.js +867 -0
- package/dist/browser.d.ts +137 -4
- package/dist/browser.js +458 -22
- package/dist/{catalogComponents-C_V39rbF-BOHveMWa.js → catalogComponents-DV7cPpUm-C77AEEx9.js} +477 -157
- package/dist/{dist-rorEJsNi.js → dist-6C03DShK.js} +654 -298
- package/dist/{dist-ZKaM8j06.js → dist-bewtXYlO.js} +1061 -807
- package/dist/governance.d.ts +87 -10
- package/dist/governance.js +194 -24
- package/dist/{index-wiV5X8Rz.d.ts → index-Bsqg4ftU.d.ts} +151 -143
- package/dist/index-D_7ZZjkl.d.ts +411 -0
- package/dist/{index-8d-oEDww.d.ts → index-vBz_FC9w.d.ts} +289 -24
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +1 -1
- package/dist/memoryArtifactStore-BtOeB_hm-tj3fC5ip.js +78 -0
- package/dist/node.d.ts +8 -4
- package/dist/node.js +1 -1
- package/dist/{openUiLibrary-B8-Cvou9-BbpNTXS3.js → openUiLibrary-W3Ce896k-ClFTRZFs.js} +6 -5
- package/dist/{skillVersionStore-DOEI9ptb-BxbYL70B.d.ts → skillVersionStore-BzLbzFOL-CxwIewHJ.d.ts} +43 -11
- package/dist/{testing-CsrG3XLz.js → testing-DDCJWvgA.js} +7 -5
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +2 -2
- package/dist/{types-AmKCKJn_-VGabeXK4.d.ts → types-D_hoCri8-BnNPiZCi.d.ts} +111 -72
- package/dist/ui-react.d.ts +352 -20
- package/dist/ui-react.js +3804 -3478
- package/dist/ui-vue.d.ts +1 -1
- package/dist/ui-vue.js +25 -6
- package/dist/ui.d.ts +4 -3
- package/dist/ui.js +3 -3
- package/dist/{webskillLitCatalog-CNaUpasU-BslMcxRZ.js → webskillLitCatalog-_mugzRHx-DiuJpCuf.js} +398 -122
- package/package.json +6 -1
- package/dist/jsonRenderRegistry-9GrWP_hE-U6Do3Kid.js +0 -2468
- package/dist/memoryArtifactStore-C9lFVqPF-yFz6yJj0.js +0 -48
package/dist/governance.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { P as FileSystemProvider, Q as
|
|
2
|
-
import {
|
|
3
|
-
import { a as AuditLog, c as
|
|
1
|
+
import { B as PageQuery, P as FileSystemProvider, Q as SkillCatalogEntry, b as UiBridge, et as SkillDocument, f as LlmMessage, it as SkillManagerPort, l as LlmClient, z as Page } from "./types-D_hoCri8-BnNPiZCi.js";
|
|
2
|
+
import { Et as SkillOutcomeReporter, L as IntegrityVerdict, Ot as SkillStateGuard, Tt as SkillIntegrityGuard, Vt as WebSkillRuntime, pt as RuntimeRun } from "./index-vBz_FC9w.js";
|
|
3
|
+
import { _ as SkillVersion, a as AuditLog, c as CandidateFile, d as CandidateSource, f as CandidateStatus, g as SkillState, h as SKILL_VERSION_PAGE_SIZE, i as AuditEvent, l as CandidateRisk, m as CompositeApprovalPolicy, n as ApprovalDecision, o as AuditQueryFilter, p as CandidateStore, r as ApprovalPolicy, s as CANDIDATE_PAGE_SIZE, t as AlwaysHumanApprovalPolicy, u as CandidateSkill, v as SkillVersionStore, y as candidateToCatalogEntry } from "./skillVersionStore-BzLbzFOL-CxwIewHJ.js";
|
|
4
4
|
//#region ../governance/dist/index.d.ts
|
|
5
5
|
//#region src/candidate/candidateNormalizer.d.ts
|
|
6
6
|
/** 剥 markdown fence 与 <think> 块、截取首尾 {};非对象 → CANDIDATE_INVALID */
|
|
@@ -32,6 +32,48 @@ declare function normalizeCandidate(input: {
|
|
|
32
32
|
/** 候选校验:缺 SKILL.md / 非法扩展名 → CANDIDATE_INVALID */
|
|
33
33
|
declare function validateCandidate(candidate: CandidateSkill): void;
|
|
34
34
|
//#endregion
|
|
35
|
+
//#region src/candidate/candidateSink.d.ts
|
|
36
|
+
/**
|
|
37
|
+
* agent 侧 `SkillCandidateSink.submit()` 的入参形状。
|
|
38
|
+
* 这里按结构声明而不是 import `@webskill/agent`:治理层是可选层,
|
|
39
|
+
* 不该因为 agent 策略而多一条运行时依赖(两侧的兼容性由测试钉住)。
|
|
40
|
+
*/
|
|
41
|
+
interface GeneratedSkillSubmission {
|
|
42
|
+
draft: {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
/** SKILL.md 全文,含 frontmatter */
|
|
46
|
+
content: string;
|
|
47
|
+
files?: readonly {
|
|
48
|
+
path: string;
|
|
49
|
+
content: string;
|
|
50
|
+
}[];
|
|
51
|
+
};
|
|
52
|
+
/** 触发生成的会话,写进候选 metadata 供 console 溯源 */
|
|
53
|
+
sessionId?: string;
|
|
54
|
+
/** 用户确认的时刻(FR-9.7 的「用户确认事实」) */
|
|
55
|
+
confirmedAt: string;
|
|
56
|
+
}
|
|
57
|
+
interface CandidateSinkOptions {
|
|
58
|
+
store: CandidateStore;
|
|
59
|
+
/** 缺省不写审计——但 FR-9.7 要求宿主接上它 */
|
|
60
|
+
audit?: AuditLog;
|
|
61
|
+
now?: () => string;
|
|
62
|
+
createId?: () => string;
|
|
63
|
+
}
|
|
64
|
+
interface CandidateSink {
|
|
65
|
+
submit(input: GeneratedSkillSubmission): Promise<{
|
|
66
|
+
id: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 把 agent 生成的技能草稿接进既有的候选管线(设计 02 §5)。
|
|
71
|
+
*
|
|
72
|
+
* 走的是与文档抽取、runtime-miss 完全相同的 `normalizeCandidate` → `CandidateStore.save`,
|
|
73
|
+
* 因此候选恒为 `draft`,审批/发布链路一行都不用改;差别只有 `source: 'generated'`。
|
|
74
|
+
*/
|
|
75
|
+
declare function createCandidateSink(options: CandidateSinkOptions): CandidateSink;
|
|
76
|
+
//#endregion
|
|
35
77
|
//#region src/candidate/llmCandidateGenerator.d.ts
|
|
36
78
|
/** LLM 候选生成器:prompt 强制约束 + 不信任归一化管线 + candidate.created 审计 */
|
|
37
79
|
declare class LlmCandidateGenerator {
|
|
@@ -50,6 +92,8 @@ declare class LlmCandidateGenerator {
|
|
|
50
92
|
}
|
|
51
93
|
//#endregion
|
|
52
94
|
//#region src/audit/fsAuditLog.d.ts
|
|
95
|
+
/** 审计查询的缺省页长。缺省属于实现,不属于调用方 */
|
|
96
|
+
declare const AUDIT_PAGE_SIZE = 100;
|
|
53
97
|
interface AuditChainVerification {
|
|
54
98
|
ok: boolean;
|
|
55
99
|
/** 首个断链位置(行号,0 起;ok 时 undefined) */
|
|
@@ -74,11 +118,15 @@ declare class FsAuditLog implements AuditLog {
|
|
|
74
118
|
id?: string;
|
|
75
119
|
ts?: string;
|
|
76
120
|
}): Promise<AuditEvent>;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
121
|
+
/**
|
|
122
|
+
* 分页 + 筛选下推。整文件读仍在(JSONL 布局未改,备案 D25),
|
|
123
|
+
* 但**跨出端口的事件数**已由 `limit` 封顶,且筛选是端口参数而非上层 `.filter()`。
|
|
124
|
+
*
|
|
125
|
+
* 本页范围内的 hash 链必须自洽:逐条重算 hash 并核对与前一行的 `prevHash` 链接,
|
|
126
|
+
* 任一处对不上以 `GOVERNANCE_FAILED` 报错。范围之外的断链不在本方法职责内——
|
|
127
|
+
* 那是 `verifyChain()` 的事(全量 O(N) 校验不该压在每次翻页上)。
|
|
128
|
+
*/
|
|
129
|
+
query(filter?: AuditQueryFilter & PageQuery): Promise<Page<AuditEvent>>;
|
|
82
130
|
/** hash 链完整性校验:逐行重算 hash 并核对 prevHash 链接 */
|
|
83
131
|
verifyChain(): Promise<AuditChainVerification>;
|
|
84
132
|
}
|
|
@@ -144,7 +192,11 @@ declare class SkillStatePolicy {
|
|
|
144
192
|
actor?: string;
|
|
145
193
|
reason?: string;
|
|
146
194
|
}): Promise<void>;
|
|
147
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* 失败计数;达阈值(默认 3)→ quarantined + 审计。
|
|
197
|
+
*
|
|
198
|
+
* 计数是**累计**而非「连续」:成功不清零,只有 `setState(name, 'active')` 会清。
|
|
199
|
+
*/
|
|
148
200
|
recordFailure(skillName: string, input?: {
|
|
149
201
|
actor?: string;
|
|
150
202
|
}): Promise<SkillState>;
|
|
@@ -156,6 +208,31 @@ declare class SkillStatePolicy {
|
|
|
156
208
|
* quarantined/disabled 三入口全拦截。
|
|
157
209
|
*/
|
|
158
210
|
toSkillStateGuard(): SkillStateGuard;
|
|
211
|
+
/**
|
|
212
|
+
* runtime SkillIntegrityGuard 适配(D3):激活期校验失败 → 置 `quarantined` + 写审计。
|
|
213
|
+
*
|
|
214
|
+
* 需求 §3 任务 3 要的是「使技能进入不可用状态,而不是只抛错」——只抛错的话,
|
|
215
|
+
* 同一个坏技能下一次激活还会再试一遍。置了状态之后,后续激活先被
|
|
216
|
+
* `toSkillStateGuard().canActivate` 挡住,根本走不到校验。
|
|
217
|
+
*
|
|
218
|
+
* `verify` 由宿主传入:完整性校验的实现在 `@webskill/node` / `@webskill/browser`,
|
|
219
|
+
* governance 与 runtime 都不能依赖它们,装配点只能在宿主。
|
|
220
|
+
*/
|
|
221
|
+
toSkillIntegrityGuard(verify: (skillName: string) => Promise<IntegrityVerdict>): SkillIntegrityGuard;
|
|
222
|
+
/**
|
|
223
|
+
* runtime `SkillOutcomeReporter` 适配(F1):一次真实的技能脚本执行失败 → `recordFailure`,
|
|
224
|
+
* 达阈值即自动 `quarantined` + 写审计。
|
|
225
|
+
*
|
|
226
|
+
* 0.4.0 之前 `recordFailure` 没有任何生产调用方——`states.json` 的失败计数只在单测里增长,
|
|
227
|
+
* 于是「失败达阈值自动隔离」在真实部署里从不触发,Console 上那个计数恒为 0。
|
|
228
|
+
* 这个适配器就是把那一段接上:runtime 不能反向依赖治理,装配点只能在宿主。
|
|
229
|
+
*
|
|
230
|
+
* 只上报失败、不上报成功,与 `recordFailure` 的既有语义保持一致(累计计数,
|
|
231
|
+
* 成功不清零,只有 `setState(name, 'active')` 清零)。
|
|
232
|
+
*/
|
|
233
|
+
toSkillOutcomeReporter(input?: {
|
|
234
|
+
actor?: string;
|
|
235
|
+
}): SkillOutcomeReporter;
|
|
159
236
|
/** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
|
|
160
237
|
assertExecutable(skillName: string): Promise<void>;
|
|
161
238
|
/** runtime catalogFilter 实现:quarantined/deprecated/disabled 技能被路由过滤 */
|
|
@@ -328,4 +405,4 @@ declare function createMissHook(deps: {
|
|
|
328
405
|
*/
|
|
329
406
|
declare function completeText(llm: LlmClient, messages: LlmMessage[]): Promise<string>;
|
|
330
407
|
//#endregion
|
|
331
|
-
export { AlwaysHumanApprovalPolicy, type ApprovalDecision, type ApprovalPolicy, type AuditEvent, type AuditLog, type CandidateFile, type CandidateRisk, type CandidateSkill, type CandidateSource, type CandidateStatus, CandidateStore, CompositeApprovalPolicy, DependencyGraph, DocumentSkillExtractor, type EvaluationReport, type EvaluationResult, EvaluationRunner, type EvaluationTask, FailureAnalyzer, type FailureDiagnosis, FsAuditLog, LlmCandidateGenerator, type RepairOption, RepairPlanner, SCORING_WEIGHTS, type ScoreInput, SimilarityDetector, SkillScorer, type SkillState, SkillStatePolicy, type SkillVersion, SkillVersionStore, type SourceDocument, candidateToCatalogEntry, completeText, createMissHook, jaccardSimilarity, normalizeCandidate, normalizeCandidateFiles, normalizeRisk, parseJsonObject, readDocument, sanitizeCandidateName, suggestFromFailedRun, validateCandidate };
|
|
408
|
+
export { AUDIT_PAGE_SIZE, AlwaysHumanApprovalPolicy, type ApprovalDecision, type ApprovalPolicy, type AuditEvent, type AuditLog, type AuditQueryFilter, CANDIDATE_PAGE_SIZE, type CandidateFile, type CandidateRisk, type CandidateSink, type CandidateSinkOptions, type CandidateSkill, type CandidateSource, type CandidateStatus, CandidateStore, CompositeApprovalPolicy, DependencyGraph, DocumentSkillExtractor, type EvaluationReport, type EvaluationResult, EvaluationRunner, type EvaluationTask, FailureAnalyzer, type FailureDiagnosis, FsAuditLog, type GeneratedSkillSubmission, LlmCandidateGenerator, type RepairOption, RepairPlanner, SCORING_WEIGHTS, SKILL_VERSION_PAGE_SIZE, type ScoreInput, SimilarityDetector, SkillScorer, type SkillState, SkillStatePolicy, type SkillVersion, SkillVersionStore, type SourceDocument, candidateToCatalogEntry, completeText, createCandidateSink, createMissHook, jaccardSimilarity, normalizeCandidate, normalizeCandidateFiles, normalizeRisk, parseJsonObject, readDocument, sanitizeCandidateName, suggestFromFailedRun, validateCandidate };
|
package/dist/governance.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { O as messageOf, T as isValidSkillName, g as assertSafePathSegment, m as WebSkillError } from "./dist-8oQRa8Xz.js";
|
|
2
|
+
import { i as textParts, n as partsToText } from "./memoryArtifactStore-BtOeB_hm-tj3fC5ip.js";
|
|
2
3
|
|
|
3
4
|
//#region ../governance/dist/index.js
|
|
4
5
|
const invalid = (message, details) => {
|
|
@@ -99,7 +100,29 @@ function validateCandidate(candidate) {
|
|
|
99
100
|
if (!candidate.files.some((f) => f.kind === "skill-md")) throw new WebSkillError("CANDIDATE_INVALID", `Candidate "${candidate.name}" is missing SKILL.md`);
|
|
100
101
|
for (const file of candidate.files) if (file.kind === "script" && !/\.(ts|js)$/.test(file.path)) throw new WebSkillError("CANDIDATE_INVALID", `Candidate "${candidate.name}" has an unsupported script file: ${file.path}`);
|
|
101
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* 治理侧各列表共用的游标切片,形状与会话消息(C1)完全一致:
|
|
105
|
+
* 无游标给**最新**一页,`nextCursor` 指向更早的历史,页内按时间升序。
|
|
106
|
+
*
|
|
107
|
+
* 缺省页长由各 store 传入——缺省属于实现,不属于调用方。
|
|
108
|
+
*/
|
|
109
|
+
function takeTailPage(all, options, defaultLimit, what) {
|
|
110
|
+
const limit = options.limit ?? defaultLimit;
|
|
111
|
+
if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
|
|
112
|
+
let end = all.length;
|
|
113
|
+
if (options.cursor !== void 0) {
|
|
114
|
+
end = Number(options.cursor);
|
|
115
|
+
if (!Number.isInteger(end) || end < 0 || end > all.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid ${what} cursor: ${JSON.stringify(options.cursor)}`);
|
|
116
|
+
}
|
|
117
|
+
const start = Math.max(0, end - limit);
|
|
118
|
+
return {
|
|
119
|
+
items: all.slice(start, end),
|
|
120
|
+
...start > 0 ? { nextCursor: String(start) } : {}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
102
123
|
const dirOf$1 = (root) => `${root}/.webskill/candidates`;
|
|
124
|
+
/** 候选列表的缺省页长。缺省属于实现,不属于调用方 */
|
|
125
|
+
const CANDIDATE_PAGE_SIZE = 50;
|
|
103
126
|
/** 逐文件持久化的候选存储:<managedRoot>/.webskill/candidates/<id>.json */
|
|
104
127
|
var CandidateStore = class {
|
|
105
128
|
#root;
|
|
@@ -119,15 +142,23 @@ var CandidateStore = class {
|
|
|
119
142
|
if (!await this.#fs.exists(path)) throw new WebSkillError("GOVERNANCE_FAILED", `Candidate not found: ${id}`);
|
|
120
143
|
return JSON.parse(await this.#fs.readText(path));
|
|
121
144
|
}
|
|
122
|
-
|
|
145
|
+
/**
|
|
146
|
+
* 候选(= 审批队列)分页。`status` 是端口参数而不是上层 `.filter()`:
|
|
147
|
+
* 审批页面只看 `pending-review`,没理由把已发布的候选全部拉过端口。
|
|
148
|
+
* 目录扫描仍是逐文件读(备案 D25),但返回量已由 `limit` 封顶。
|
|
149
|
+
*/
|
|
150
|
+
async list(options = {}) {
|
|
123
151
|
const dir = dirOf$1(this.#root);
|
|
124
|
-
if (!await this.#fs.exists(dir)) return [];
|
|
152
|
+
if (!await this.#fs.exists(dir)) return { items: [] };
|
|
125
153
|
const out = [];
|
|
126
154
|
for (const entry of await this.#fs.list(dir)) {
|
|
127
155
|
if (entry.type !== "file" || !entry.path.endsWith(".json")) continue;
|
|
128
|
-
|
|
156
|
+
const candidate = JSON.parse(await this.#fs.readText(entry.path));
|
|
157
|
+
if (options.status !== void 0 && candidate.status !== options.status) continue;
|
|
158
|
+
out.push(candidate);
|
|
129
159
|
}
|
|
130
|
-
|
|
160
|
+
out.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
161
|
+
return takeTailPage(out, options, 50, "candidate");
|
|
131
162
|
}
|
|
132
163
|
async updateStatus(id, status, now) {
|
|
133
164
|
const candidate = await this.get(id);
|
|
@@ -151,6 +182,49 @@ function candidateToCatalogEntry(candidate) {
|
|
|
151
182
|
};
|
|
152
183
|
}
|
|
153
184
|
/**
|
|
185
|
+
* 把 agent 生成的技能草稿接进既有的候选管线(设计 02 §5)。
|
|
186
|
+
*
|
|
187
|
+
* 走的是与文档抽取、runtime-miss 完全相同的 `normalizeCandidate` → `CandidateStore.save`,
|
|
188
|
+
* 因此候选恒为 `draft`,审批/发布链路一行都不用改;差别只有 `source: 'generated'`。
|
|
189
|
+
*/
|
|
190
|
+
function createCandidateSink(options) {
|
|
191
|
+
return { async submit(input) {
|
|
192
|
+
const { draft } = input;
|
|
193
|
+
const candidate = normalizeCandidate({
|
|
194
|
+
raw: {
|
|
195
|
+
name: draft.name,
|
|
196
|
+
description: draft.description,
|
|
197
|
+
files: [{
|
|
198
|
+
path: "SKILL.md",
|
|
199
|
+
content: draft.content
|
|
200
|
+
}, ...draft.files ?? []]
|
|
201
|
+
},
|
|
202
|
+
source: "generated",
|
|
203
|
+
...options.now ? { now: options.now } : {},
|
|
204
|
+
...options.createId ? { createId: options.createId } : {}
|
|
205
|
+
});
|
|
206
|
+
candidate.metadata = {
|
|
207
|
+
...candidate.metadata,
|
|
208
|
+
confirmedAt: input.confirmedAt,
|
|
209
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {}
|
|
210
|
+
};
|
|
211
|
+
await options.store.save(candidate);
|
|
212
|
+
await options.audit?.append({
|
|
213
|
+
type: "candidate.created",
|
|
214
|
+
target: candidate.id,
|
|
215
|
+
data: {
|
|
216
|
+
name: candidate.name,
|
|
217
|
+
source: candidate.source,
|
|
218
|
+
risk: candidate.risk,
|
|
219
|
+
userConfirmed: true,
|
|
220
|
+
confirmedAt: input.confirmedAt,
|
|
221
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return { id: candidate.id };
|
|
225
|
+
} };
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
154
228
|
* 文本补全:优先走流式(慢速本地模型下非流式整响应缓冲会触发 fetch 头超时,
|
|
155
229
|
* SSE 首包秒到不受此限);无 stream 的客户端回退 complete。
|
|
156
230
|
*/
|
|
@@ -162,7 +236,7 @@ async function completeText(llm, messages) {
|
|
|
162
236
|
else if (event.type === "done") doneContent = event.content;
|
|
163
237
|
return doneContent ?? content;
|
|
164
238
|
}
|
|
165
|
-
return (await llm.complete({ messages })).content
|
|
239
|
+
return partsToText((await llm.complete({ messages })).content);
|
|
166
240
|
}
|
|
167
241
|
const GENERATION_PROMPT = (task, context) => [
|
|
168
242
|
"You generate a skill candidate as STRICT JSON only (no markdown, no explanation).",
|
|
@@ -193,7 +267,7 @@ var LlmCandidateGenerator = class {
|
|
|
193
267
|
const candidate = normalizeCandidate({
|
|
194
268
|
raw: parseJsonObject(await completeText(this.#llm, [{
|
|
195
269
|
role: "user",
|
|
196
|
-
content: GENERATION_PROMPT(input.prompt, input.context)
|
|
270
|
+
content: textParts(GENERATION_PROMPT(input.prompt, input.context))
|
|
197
271
|
}])),
|
|
198
272
|
source: input.source,
|
|
199
273
|
...this.#now ? { now: this.#now } : {},
|
|
@@ -237,6 +311,8 @@ var CompositeApprovalPolicy = class {
|
|
|
237
311
|
}
|
|
238
312
|
};
|
|
239
313
|
const fileOf$1 = (root) => `${root}/.webskill/audit.jsonl`;
|
|
314
|
+
/** 审计查询的缺省页长。缺省属于实现,不属于调用方 */
|
|
315
|
+
const AUDIT_PAGE_SIZE = 100;
|
|
240
316
|
/** 环境无关 sha256(WebCrypto;Node ≥17 与浏览器均有 globalThis.crypto.subtle) */
|
|
241
317
|
const sha256Hex$1 = async (text) => {
|
|
242
318
|
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
@@ -304,28 +380,67 @@ var FsAuditLog = class {
|
|
|
304
380
|
this.#lastHash = full.hash;
|
|
305
381
|
return full;
|
|
306
382
|
}
|
|
307
|
-
|
|
383
|
+
/**
|
|
384
|
+
* 分页 + 筛选下推。整文件读仍在(JSONL 布局未改,备案 D25),
|
|
385
|
+
* 但**跨出端口的事件数**已由 `limit` 封顶,且筛选是端口参数而非上层 `.filter()`。
|
|
386
|
+
*
|
|
387
|
+
* 本页范围内的 hash 链必须自洽:逐条重算 hash 并核对与前一行的 `prevHash` 链接,
|
|
388
|
+
* 任一处对不上以 `GOVERNANCE_FAILED` 报错。范围之外的断链不在本方法职责内——
|
|
389
|
+
* 那是 `verifyChain()` 的事(全量 O(N) 校验不该压在每次翻页上)。
|
|
390
|
+
*/
|
|
391
|
+
async query(filter = {}) {
|
|
308
392
|
const path = fileOf$1(this.#root);
|
|
309
|
-
if (!await this.#fs.exists(path)) return [];
|
|
393
|
+
if (!await this.#fs.exists(path)) return { items: [] };
|
|
310
394
|
const raw = await this.#fs.readText(path);
|
|
311
|
-
|
|
395
|
+
/** 按文件行序保留(含解析失败的洞):链校验必须在文件行序上做,不能在筛选结果上做 */
|
|
396
|
+
const records = [];
|
|
312
397
|
let skippedLines = 0;
|
|
313
398
|
for (const line of raw.split("\n")) {
|
|
314
399
|
if (line.trim() === "") continue;
|
|
315
|
-
let event;
|
|
316
400
|
try {
|
|
317
|
-
|
|
401
|
+
records.push(JSON.parse(line));
|
|
318
402
|
} catch {
|
|
319
403
|
skippedLines += 1;
|
|
320
|
-
|
|
404
|
+
records.push(void 0);
|
|
321
405
|
}
|
|
406
|
+
}
|
|
407
|
+
if (skippedLines > 0) console.warn(`[webskill] Audit log at ${path} contains ${skippedLines} unparsable line(s) skipped by query; run verifyChain() to check integrity`);
|
|
408
|
+
const matched = [];
|
|
409
|
+
for (let i = 0; i < records.length; i++) {
|
|
410
|
+
const event = records[i];
|
|
411
|
+
if (event === void 0) continue;
|
|
322
412
|
if (filter.target !== void 0 && event.target !== filter.target) continue;
|
|
323
413
|
if (filter.type !== void 0 && event.type !== filter.type) continue;
|
|
414
|
+
if (filter.actor !== void 0 && event.actor !== filter.actor) continue;
|
|
324
415
|
if (filter.since !== void 0 && event.ts < filter.since) continue;
|
|
325
|
-
|
|
416
|
+
if (filter.until !== void 0 && event.ts > filter.until) continue;
|
|
417
|
+
matched.push(i);
|
|
326
418
|
}
|
|
327
|
-
|
|
328
|
-
|
|
419
|
+
const limit = filter.limit ?? 100;
|
|
420
|
+
if (!Number.isInteger(limit) || limit <= 0) throw new WebSkillError("VALIDATION_FAILED", `Page limit must be a positive integer, received ${String(limit)}`);
|
|
421
|
+
let end = matched.length;
|
|
422
|
+
if (filter.cursor !== void 0) {
|
|
423
|
+
end = Number(filter.cursor);
|
|
424
|
+
if (!Number.isInteger(end) || end < 0 || end > matched.length) throw new WebSkillError("VALIDATION_FAILED", `Invalid audit cursor: ${JSON.stringify(filter.cursor)}`);
|
|
425
|
+
}
|
|
426
|
+
const start = Math.max(0, end - limit);
|
|
427
|
+
const items = [];
|
|
428
|
+
for (const line of matched.slice(start, end)) {
|
|
429
|
+
const event = records[line];
|
|
430
|
+
await this.#assertChainAt(records, line, path);
|
|
431
|
+
items.push(event);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
items,
|
|
435
|
+
...start > 0 ? { nextCursor: String(start) } : {}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** 单行的链自洽:本行 hash 重算相符,且 prevHash 指向前一行的 hash */
|
|
439
|
+
async #assertChainAt(records, index, path) {
|
|
440
|
+
const event = records[index];
|
|
441
|
+
const expectedPrev = index === 0 ? "GENESIS" : records[index - 1]?.hash;
|
|
442
|
+
if (expectedPrev === void 0 || event.prevHash !== expectedPrev) throw new WebSkillError("GOVERNANCE_FAILED", `Audit chain is broken at line ${index} of ${path}: prevHash link mismatch (events may have been removed, reordered or corrupted)`);
|
|
443
|
+
if (event.hash !== await sha256Hex$1(canonical(event))) throw new WebSkillError("GOVERNANCE_FAILED", `Audit chain is broken at line ${index} of ${path}: event hash mismatch (event content was tampered)`);
|
|
329
444
|
}
|
|
330
445
|
/** hash 链完整性校验:逐行重算 hash 并核对 prevHash 链接 */
|
|
331
446
|
async verifyChain() {
|
|
@@ -364,6 +479,8 @@ const dirOf = (root, skillName) => {
|
|
|
364
479
|
assertSafePathSegment(skillName, "skill name");
|
|
365
480
|
return `${root}/.webskill/versions/${skillName}`;
|
|
366
481
|
};
|
|
482
|
+
/** 版本列表的缺省页长。缺省属于实现,不属于调用方 */
|
|
483
|
+
const SKILL_VERSION_PAGE_SIZE = 20;
|
|
367
484
|
/** 版本存储:manifest 快照 + parentVersionId 链;回滚 = 追加新版本(谱系不断)。
|
|
368
485
|
* 保留策略:maxArchivesPerSkill(默认 5)超出时清理最旧版本(json + zip 归档一并删除)。 */
|
|
369
486
|
var SkillVersionStore = class {
|
|
@@ -382,7 +499,7 @@ var SkillVersionStore = class {
|
|
|
382
499
|
this.#maxArchives = deps.maxArchivesPerSkill ?? 5;
|
|
383
500
|
}
|
|
384
501
|
async add(skillName, input) {
|
|
385
|
-
const existing = await this
|
|
502
|
+
const existing = await this.#readAll(skillName);
|
|
386
503
|
const version = {
|
|
387
504
|
versionId: this.#createId?.() ?? `v-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
388
505
|
parentVersionId: input.parentVersionId ?? existing.at(-1)?.versionId,
|
|
@@ -402,7 +519,7 @@ var SkillVersionStore = class {
|
|
|
402
519
|
}
|
|
403
520
|
/** 保留策略:超出 maxArchivesPerSkill 时按 createdAt 清理最旧版本(json + zip) */
|
|
404
521
|
async #prune(skillName) {
|
|
405
|
-
const versions = await this
|
|
522
|
+
const versions = await this.#readAll(skillName);
|
|
406
523
|
const excess = versions.length - this.#maxArchives;
|
|
407
524
|
if (excess <= 0) return;
|
|
408
525
|
for (const old of versions.slice(0, excess)) {
|
|
@@ -416,7 +533,14 @@ var SkillVersionStore = class {
|
|
|
416
533
|
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)`);
|
|
417
534
|
return this.#fs.readBinary(version.archivePath);
|
|
418
535
|
}
|
|
419
|
-
|
|
536
|
+
/**
|
|
537
|
+
* 版本列表分页(方向同 C1:无游标给**最新**一页)。
|
|
538
|
+
* 保留策略与谱系构建走 `#readAll`:它们必须看到全量,不能被分页截断。
|
|
539
|
+
*/
|
|
540
|
+
async list(skillName, options = {}) {
|
|
541
|
+
return takeTailPage(await this.#readAll(skillName), options, 20, "version");
|
|
542
|
+
}
|
|
543
|
+
async #readAll(skillName) {
|
|
420
544
|
const dir = dirOf(this.#root, skillName);
|
|
421
545
|
if (!await this.#fs.exists(dir)) return [];
|
|
422
546
|
const out = [];
|
|
@@ -472,15 +596,15 @@ var FailureAnalyzer = class {
|
|
|
472
596
|
const summary = failed.slice(0, 10).map((e) => `${e.type}: ${e.message ?? ""} ${JSON.stringify(e.data ?? {})}`).join("\n");
|
|
473
597
|
const response = await this.#llm.complete({ messages: [{
|
|
474
598
|
role: "user",
|
|
475
|
-
content: [
|
|
599
|
+
content: textParts([
|
|
476
600
|
"Diagnose this failed agent run as STRICT JSON only:",
|
|
477
601
|
"{\"cause\": string, \"suggestedFix\": string, \"errorCode\"?: string}",
|
|
478
602
|
`Run termination: ${run.terminationReason ?? "unknown"}`,
|
|
479
603
|
`Failures:\n${summary || "(none recorded)"}`
|
|
480
|
-
].join("\n")
|
|
604
|
+
].join("\n"))
|
|
481
605
|
}] });
|
|
482
606
|
try {
|
|
483
|
-
const parsed = parseJsonObject(response.content
|
|
607
|
+
const parsed = parseJsonObject(partsToText(response.content));
|
|
484
608
|
return {
|
|
485
609
|
cause: String(parsed["cause"] ?? "Unknown cause"),
|
|
486
610
|
suggestedFix: String(parsed["suggestedFix"] ?? "Manual inspection required"),
|
|
@@ -596,7 +720,11 @@ var SkillStatePolicy = class {
|
|
|
596
720
|
...input.reason !== void 0 ? { data: { reason: input.reason } } : {}
|
|
597
721
|
});
|
|
598
722
|
}
|
|
599
|
-
/**
|
|
723
|
+
/**
|
|
724
|
+
* 失败计数;达阈值(默认 3)→ quarantined + 审计。
|
|
725
|
+
*
|
|
726
|
+
* 计数是**累计**而非「连续」:成功不清零,只有 `setState(name, 'active')` 会清。
|
|
727
|
+
*/
|
|
600
728
|
async recordFailure(skillName, input = {}) {
|
|
601
729
|
assertSafePathSegment(skillName, "skill name");
|
|
602
730
|
const data = await this.#load();
|
|
@@ -640,6 +768,48 @@ var SkillStatePolicy = class {
|
|
|
640
768
|
canExecute: async (skillName) => this.canExecute(await this.getState(skillName))
|
|
641
769
|
};
|
|
642
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* runtime SkillIntegrityGuard 适配(D3):激活期校验失败 → 置 `quarantined` + 写审计。
|
|
773
|
+
*
|
|
774
|
+
* 需求 §3 任务 3 要的是「使技能进入不可用状态,而不是只抛错」——只抛错的话,
|
|
775
|
+
* 同一个坏技能下一次激活还会再试一遍。置了状态之后,后续激活先被
|
|
776
|
+
* `toSkillStateGuard().canActivate` 挡住,根本走不到校验。
|
|
777
|
+
*
|
|
778
|
+
* `verify` 由宿主传入:完整性校验的实现在 `@webskill/node` / `@webskill/browser`,
|
|
779
|
+
* governance 与 runtime 都不能依赖它们,装配点只能在宿主。
|
|
780
|
+
*/
|
|
781
|
+
toSkillIntegrityGuard(verify) {
|
|
782
|
+
return { verifyOnActivate: async (skillName) => {
|
|
783
|
+
assertSafePathSegment(skillName, "skill name");
|
|
784
|
+
let verdict;
|
|
785
|
+
try {
|
|
786
|
+
verdict = await verify(skillName);
|
|
787
|
+
} catch (e) {
|
|
788
|
+
verdict = {
|
|
789
|
+
ok: false,
|
|
790
|
+
reason: `integrity check failed: ${e instanceof Error ? e.message : String(e)}`
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (!verdict.ok) await this.setState(skillName, "quarantined", { reason: verdict.reason ?? "integrity verification failed on activation" });
|
|
794
|
+
return verdict;
|
|
795
|
+
} };
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* runtime `SkillOutcomeReporter` 适配(F1):一次真实的技能脚本执行失败 → `recordFailure`,
|
|
799
|
+
* 达阈值即自动 `quarantined` + 写审计。
|
|
800
|
+
*
|
|
801
|
+
* 0.4.0 之前 `recordFailure` 没有任何生产调用方——`states.json` 的失败计数只在单测里增长,
|
|
802
|
+
* 于是「失败达阈值自动隔离」在真实部署里从不触发,Console 上那个计数恒为 0。
|
|
803
|
+
* 这个适配器就是把那一段接上:runtime 不能反向依赖治理,装配点只能在宿主。
|
|
804
|
+
*
|
|
805
|
+
* 只上报失败、不上报成功,与 `recordFailure` 的既有语义保持一致(累计计数,
|
|
806
|
+
* 成功不清零,只有 `setState(name, 'active')` 清零)。
|
|
807
|
+
*/
|
|
808
|
+
toSkillOutcomeReporter(input = {}) {
|
|
809
|
+
return { onSkillFailed: async (report) => {
|
|
810
|
+
await this.recordFailure(report.skillName, input);
|
|
811
|
+
} };
|
|
812
|
+
}
|
|
643
813
|
/** disabled → SKILL_DISABLED;quarantined → SKILL_QUARANTINED */
|
|
644
814
|
async assertExecutable(skillName) {
|
|
645
815
|
const state = await this.getState(skillName);
|
|
@@ -863,7 +1033,7 @@ var DocumentSkillExtractor = class {
|
|
|
863
1033
|
const candidate = normalizeCandidate({
|
|
864
1034
|
raw: parseJsonObject(await completeText(this.#llm, [{
|
|
865
1035
|
role: "user",
|
|
866
|
-
content: EXTRACT_PROMPT(input.document, input.nameHint)
|
|
1036
|
+
content: textParts(EXTRACT_PROMPT(input.document, input.nameHint))
|
|
867
1037
|
}])),
|
|
868
1038
|
source: "document",
|
|
869
1039
|
...this.#now ? { now: this.#now } : {},
|
|
@@ -919,4 +1089,4 @@ function createMissHook(deps) {
|
|
|
919
1089
|
}
|
|
920
1090
|
|
|
921
1091
|
//#endregion
|
|
922
|
-
export { AlwaysHumanApprovalPolicy, CandidateStore, CompositeApprovalPolicy, DependencyGraph, DocumentSkillExtractor, EvaluationRunner, FailureAnalyzer, FsAuditLog, LlmCandidateGenerator, RepairPlanner, SCORING_WEIGHTS, SimilarityDetector, SkillScorer, SkillStatePolicy, SkillVersionStore, candidateToCatalogEntry, completeText, createMissHook, jaccardSimilarity, normalizeCandidate, normalizeCandidateFiles, normalizeRisk, parseJsonObject, readDocument, sanitizeCandidateName, suggestFromFailedRun, validateCandidate };
|
|
1092
|
+
export { AUDIT_PAGE_SIZE, AlwaysHumanApprovalPolicy, CANDIDATE_PAGE_SIZE, CandidateStore, CompositeApprovalPolicy, DependencyGraph, DocumentSkillExtractor, EvaluationRunner, FailureAnalyzer, FsAuditLog, LlmCandidateGenerator, RepairPlanner, SCORING_WEIGHTS, SKILL_VERSION_PAGE_SIZE, SimilarityDetector, SkillScorer, SkillStatePolicy, SkillVersionStore, candidateToCatalogEntry, completeText, createCandidateSink, createMissHook, jaccardSimilarity, normalizeCandidate, normalizeCandidateFiles, normalizeRisk, parseJsonObject, readDocument, sanitizeCandidateName, suggestFromFailedRun, validateCandidate };
|