@yottameta/yotta-skills-plugin 0.19.17 → 0.19.18
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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-skills/SKILL.md +1 -1
- package/skills/yotta-skills/bin/yotta-skills.js +1 -1
- package/skills/yotta-skills/lib/hook-adapter.js +385 -0
- package/skills/yotta-skills/lib/install-evidence.js +18 -0
- package/skills/yotta-skills/lib/install-health.js +169 -0
- package/skills/yotta-skills/lib/install-lifecycle.js +149 -0
- package/skills/yotta-skills/lib/install-pipeline.js +387 -0
- package/skills/yotta-skills/lib/install-snapshot.js +245 -0
- package/skills/yotta-skills/lib/manifest.js +136 -0
- package/skills/yotta-skills/lib/route.js +240 -0
- package/skills/yotta-skills/lib/skills-scan.js +419 -0
- package/skills/yotta-skills/lib/trusted-verifier.js +156 -0
- package/skills/yotta-skills/lib/update-check.js +99 -0
- package/skills/yotta-skills/lib/verify-gate.js +93 -0
- package/skills/yotta-skills/references/skill-list.md +2 -2
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
- package/skills/yotta-skills/skills.json +161 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.19.
|
|
15
|
+
"version": "0.19.18",
|
|
16
16
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元阁 yotta-skills",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Developer Tools",
|
|
15
|
-
"version": "0.19.
|
|
15
|
+
"version": "0.19.18",
|
|
16
16
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元阁 yotta-skills",
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "yotta-skills",
|
|
4
|
-
"version": "0.19.
|
|
4
|
+
"version": "0.19.18",
|
|
5
5
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "YottaMeta",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-skills
|
|
3
|
-
version: 0.19.
|
|
3
|
+
version: 0.19.18
|
|
4
4
|
description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点 + 运行时 hook 适配。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么组合使用(安装与调用均由用户确认后执行)」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录(默认 --pin 锁死清单精确版本);盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工只建议跑本地 --reindex;更新检查走手动 --check 或后台 --check --scheduled);运行时适配层:hook capabilities / evaluate / bind / unbind 按宿主能力矩阵执行六个统一事件并留证降级,元信 before_install 已接入安装管线;自包含零依赖,不依赖任何元技能;MCP 按需加载且需用户确认后写入配置(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --check --scheduled(后台周检)/ update --auto(家族自动更新)/ hook 适配 / --inventory / --reindex / --dry-run 预览 / --pin(默认)/ --range。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、评估宿主 hook 能力、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/hook 适配/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index + hook 适配」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能、不静默写宿主配置或全局记忆;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
@@ -38,7 +38,7 @@ const hookAdapterLib = require('../lib/hook-adapter');
|
|
|
38
38
|
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
39
39
|
|
|
40
40
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
41
|
-
let VERSION = '0.19.
|
|
41
|
+
let VERSION = '0.19.18';
|
|
42
42
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
43
43
|
|
|
44
44
|
function loadManifest() {
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const EVENTS = [
|
|
9
|
+
'before_start',
|
|
10
|
+
'before_tool',
|
|
11
|
+
'before_install',
|
|
12
|
+
'before_publish',
|
|
13
|
+
'after_milestone',
|
|
14
|
+
'before_send',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const CAPABILITY_LEVELS = [
|
|
18
|
+
'native-block',
|
|
19
|
+
'native-audit',
|
|
20
|
+
'wrapper-only',
|
|
21
|
+
'unsupported',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const EVIDENCE_KINDS = [
|
|
25
|
+
'tool_call_id',
|
|
26
|
+
'hook_event_id',
|
|
27
|
+
'host_transcript',
|
|
28
|
+
'file_path',
|
|
29
|
+
'exit_code',
|
|
30
|
+
'audit_log',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const FALLBACKS = ['wrapper', 'advisory', 'explicit-unverified'];
|
|
34
|
+
|
|
35
|
+
const CODEX_CAPABILITIES = {
|
|
36
|
+
before_start: 'native-audit',
|
|
37
|
+
before_tool: 'native-audit',
|
|
38
|
+
before_install: 'wrapper-only',
|
|
39
|
+
before_publish: 'wrapper-only',
|
|
40
|
+
after_milestone: 'native-audit',
|
|
41
|
+
before_send: 'unsupported',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function unsupportedCapabilities() {
|
|
45
|
+
return EVENTS.reduce((out, event) => {
|
|
46
|
+
out[event] = 'unsupported';
|
|
47
|
+
return out;
|
|
48
|
+
}, {});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function capabilitiesForHost(host) {
|
|
52
|
+
if (host === 'codex') return { ...CODEX_CAPABILITIES };
|
|
53
|
+
return unsupportedCapabilities();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateHookRequirements(manifest) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
const requirements = manifest && Array.isArray(manifest.hooks) ? manifest.hooks : [];
|
|
59
|
+
requirements.forEach((requirement, index) => {
|
|
60
|
+
const prefix = 'hooks[' + index + ']';
|
|
61
|
+
if (!requirement || typeof requirement !== 'object' || Array.isArray(requirement)) {
|
|
62
|
+
errors.push(prefix + ': 必须是对象');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!EVENTS.includes(requirement.event)) {
|
|
66
|
+
errors.push(prefix + ': event 非法');
|
|
67
|
+
}
|
|
68
|
+
if (!['block', 'warn'].includes(requirement.on_fail)) {
|
|
69
|
+
errors.push(prefix + ': on_fail 非法');
|
|
70
|
+
}
|
|
71
|
+
if (!requirement.require_tool && !requirement.require_action) {
|
|
72
|
+
errors.push(prefix + ': 必须声明 require_tool 或 require_action');
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(requirement.evidence) || requirement.evidence.length === 0) {
|
|
75
|
+
errors.push(prefix + ': evidence 不能为空');
|
|
76
|
+
} else if (requirement.evidence.some((kind) => !EVIDENCE_KINDS.includes(kind))) {
|
|
77
|
+
errors.push(prefix + ': evidence 含不支持的来源');
|
|
78
|
+
}
|
|
79
|
+
if (requirement.fallback !== undefined && !FALLBACKS.includes(requirement.fallback)) {
|
|
80
|
+
errors.push(prefix + ': fallback 非法');
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return { errors, requirements };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function actionOf(requirement, index) {
|
|
87
|
+
return requirement.require_tool || requirement.require_action || ('hook-' + index);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkFor(context, requirement) {
|
|
91
|
+
const key = requirement.require_tool || requirement.require_action;
|
|
92
|
+
return context && context.checks && context.checks[key] ? context.checks[key] : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function evidenceFor(requirement, context, check) {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const kind of requirement.evidence || []) {
|
|
98
|
+
if (check && check.evidence && check.evidence[kind] !== undefined) out[kind] = check.evidence[kind];
|
|
99
|
+
else if (context && context.evidence && context.evidence[kind] !== undefined) out[kind] = context.evidence[kind];
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function evaluateOne(requirement, index, event, capability, context, skill) {
|
|
105
|
+
const action = actionOf(requirement, index);
|
|
106
|
+
const check = checkFor(context, requirement);
|
|
107
|
+
const evidence = evidenceFor(requirement, context, check);
|
|
108
|
+
const hasEvidence = Object.keys(evidence).length > 0;
|
|
109
|
+
const passed = !!check && check.ok === true;
|
|
110
|
+
const failed = !!check && check.ok === false;
|
|
111
|
+
const missing = !check;
|
|
112
|
+
let decision = 'allow';
|
|
113
|
+
let reason = '检查通过';
|
|
114
|
+
let correction = false;
|
|
115
|
+
|
|
116
|
+
if (passed) {
|
|
117
|
+
const canVerify = capability === 'native-block'
|
|
118
|
+
|| capability === 'native-audit'
|
|
119
|
+
|| (capability === 'wrapper-only' && context && context.wrapperRegistered === true);
|
|
120
|
+
if (!canVerify) {
|
|
121
|
+
decision = 'unverified';
|
|
122
|
+
reason = hasEvidence ? '宿主能力不足,不能声明为强制' : '缺少可验证证据';
|
|
123
|
+
} else if (!hasEvidence) {
|
|
124
|
+
decision = 'unverified';
|
|
125
|
+
reason = '缺少声明要求的可验证证据';
|
|
126
|
+
}
|
|
127
|
+
} else if (failed || missing) {
|
|
128
|
+
if (requirement.on_fail === 'warn') {
|
|
129
|
+
decision = 'warn';
|
|
130
|
+
reason = '检查未通过,按声明降级为警告';
|
|
131
|
+
} else if (capability === 'native-block') {
|
|
132
|
+
decision = 'block';
|
|
133
|
+
reason = '检查未通过,宿主支持动作前阻断';
|
|
134
|
+
} else if (capability === 'wrapper-only') {
|
|
135
|
+
if (context && context.wrapperRegistered === true) {
|
|
136
|
+
decision = 'block';
|
|
137
|
+
reason = '检查未通过,wrapper 路径可阻断';
|
|
138
|
+
} else {
|
|
139
|
+
decision = 'unverified';
|
|
140
|
+
reason = 'wrapper 未注册,无法保证拦截';
|
|
141
|
+
}
|
|
142
|
+
} else if (capability === 'native-audit') {
|
|
143
|
+
decision = 'unverified';
|
|
144
|
+
correction = true;
|
|
145
|
+
reason = '动作已发生,只能审计并触发一次纠偏';
|
|
146
|
+
} else {
|
|
147
|
+
decision = 'unverified';
|
|
148
|
+
reason = '宿主不支持该事件,按 explicit-unverified 降级';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
event,
|
|
154
|
+
skill: skill || null,
|
|
155
|
+
action,
|
|
156
|
+
capability,
|
|
157
|
+
decision,
|
|
158
|
+
passed,
|
|
159
|
+
failed,
|
|
160
|
+
missing,
|
|
161
|
+
correction,
|
|
162
|
+
evidence,
|
|
163
|
+
reason,
|
|
164
|
+
requirement,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function evaluateHook(input) {
|
|
169
|
+
const opts = input || {};
|
|
170
|
+
const event = opts.event;
|
|
171
|
+
const context = opts.context || {};
|
|
172
|
+
const manifest = opts.manifest || {};
|
|
173
|
+
const capabilities = opts.capabilities || capabilitiesForHost(opts.host || 'generic');
|
|
174
|
+
if (!EVENTS.includes(event)) {
|
|
175
|
+
return {
|
|
176
|
+
decision: 'block',
|
|
177
|
+
verified: false,
|
|
178
|
+
correction: false,
|
|
179
|
+
results: [],
|
|
180
|
+
evidence: [],
|
|
181
|
+
user_message: '阻断:未知 hook 事件 ' + event,
|
|
182
|
+
errors: ['未知 hook 事件: ' + event],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const validation = validateHookRequirements(manifest);
|
|
187
|
+
if (validation.errors.length > 0) {
|
|
188
|
+
return {
|
|
189
|
+
decision: 'block',
|
|
190
|
+
verified: false,
|
|
191
|
+
correction: false,
|
|
192
|
+
results: [],
|
|
193
|
+
evidence: [],
|
|
194
|
+
user_message: '阻断:manifest hook 声明非法',
|
|
195
|
+
errors: validation.errors,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const requirements = validation.requirements.filter((requirement) => requirement.event === event);
|
|
200
|
+
const results = requirements.map((requirement, index) => evaluateOne(
|
|
201
|
+
requirement,
|
|
202
|
+
index,
|
|
203
|
+
event,
|
|
204
|
+
capabilities[event] || 'unsupported',
|
|
205
|
+
context,
|
|
206
|
+
manifest.slug,
|
|
207
|
+
));
|
|
208
|
+
const decision = results.some((result) => result.decision === 'block')
|
|
209
|
+
? 'block'
|
|
210
|
+
: results.some((result) => result.decision === 'warn')
|
|
211
|
+
? 'warn'
|
|
212
|
+
: results.some((result) => result.decision === 'unverified')
|
|
213
|
+
? 'unverified'
|
|
214
|
+
: 'allow';
|
|
215
|
+
const evidence = results.map((result) => ({
|
|
216
|
+
event: result.event,
|
|
217
|
+
skill: result.skill,
|
|
218
|
+
action: result.action,
|
|
219
|
+
result: result.decision,
|
|
220
|
+
capability: result.capability,
|
|
221
|
+
evidence: result.evidence,
|
|
222
|
+
reason: result.reason,
|
|
223
|
+
}));
|
|
224
|
+
const correction = results.some((result) => result.correction);
|
|
225
|
+
const verified = decision === 'allow' && results.every((result) => result.decision === 'allow');
|
|
226
|
+
let userMessage = '已通过';
|
|
227
|
+
if (decision === 'block') userMessage = '阻断:' + results.filter((result) => result.decision === 'block').map((result) => result.reason).join(';');
|
|
228
|
+
else if (decision === 'warn') userMessage = '警告:' + results.filter((result) => result.decision === 'warn').map((result) => result.reason).join(';');
|
|
229
|
+
else if (decision === 'unverified') userMessage = 'explicit-unverified:' + results.filter((result) => result.decision === 'unverified').map((result) => result.reason).join(';');
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
decision,
|
|
233
|
+
verified,
|
|
234
|
+
correction,
|
|
235
|
+
results,
|
|
236
|
+
evidence,
|
|
237
|
+
user_message: userMessage,
|
|
238
|
+
errors: [],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function hookLogPath(homeDir) {
|
|
243
|
+
return path.join(homeDir || os.homedir(), '.yottaskills', 'hook-log.jsonl');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function bindingsPath(homeDir) {
|
|
247
|
+
return path.join(homeDir || os.homedir(), '.yottaskills', 'hook-bindings.json');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function appendHookEvidence(entry, options) {
|
|
251
|
+
const file = hookLogPath(options && options.homeDir);
|
|
252
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
253
|
+
const record = {
|
|
254
|
+
...entry,
|
|
255
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
256
|
+
};
|
|
257
|
+
fs.appendFileSync(file, JSON.stringify(record) + '\n', 'utf8');
|
|
258
|
+
return file;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function emptyBindings() {
|
|
262
|
+
return { version: 1, bindings: {} };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readBindings(homeDir) {
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(fs.readFileSync(bindingsPath(homeDir), 'utf8'));
|
|
268
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !parsed.bindings) return emptyBindings();
|
|
269
|
+
return { version: 1, bindings: { ...parsed.bindings } };
|
|
270
|
+
} catch (_) {
|
|
271
|
+
return emptyBindings();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function writeBindings(homeDir, value) {
|
|
276
|
+
const file = bindingsPath(homeDir);
|
|
277
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
278
|
+
const tmp = file + '.tmp-' + process.pid + '-' + Date.now();
|
|
279
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
280
|
+
try {
|
|
281
|
+
fs.renameSync(tmp, file);
|
|
282
|
+
} catch (_) {
|
|
283
|
+
fs.rmSync(file, { force: true });
|
|
284
|
+
fs.renameSync(tmp, file);
|
|
285
|
+
}
|
|
286
|
+
return file;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function bindingId(host, skill, event, action) {
|
|
290
|
+
return crypto.createHash('sha256').update([host, skill, event, action].join('|')).digest('hex').slice(0, 24);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function bindHookRequirements(input) {
|
|
294
|
+
const opts = input || {};
|
|
295
|
+
const manifest = opts.manifest || {};
|
|
296
|
+
const validation = validateHookRequirements(manifest);
|
|
297
|
+
if (validation.errors.length > 0) {
|
|
298
|
+
const error = new Error(validation.errors.join('; '));
|
|
299
|
+
error.errors = validation.errors;
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
const capabilities = capabilitiesForHost(opts.host || 'generic');
|
|
303
|
+
const selected = opts.event
|
|
304
|
+
? validation.requirements.filter((requirement) => requirement.event === opts.event)
|
|
305
|
+
: validation.requirements;
|
|
306
|
+
const registry = readBindings(opts.homeDir);
|
|
307
|
+
const records = selected.map((requirement, index) => {
|
|
308
|
+
const action = actionOf(requirement, index);
|
|
309
|
+
const id = bindingId(opts.host || 'generic', manifest.slug, requirement.event, action);
|
|
310
|
+
const previous = registry.bindings[id];
|
|
311
|
+
const record = {
|
|
312
|
+
id,
|
|
313
|
+
host: opts.host || 'generic',
|
|
314
|
+
skill: manifest.slug,
|
|
315
|
+
event: requirement.event,
|
|
316
|
+
action,
|
|
317
|
+
on_fail: requirement.on_fail,
|
|
318
|
+
fallback: requirement.fallback || 'explicit-unverified',
|
|
319
|
+
capability: capabilities[requirement.event] || 'unsupported',
|
|
320
|
+
created_at: previous ? previous.created_at : new Date().toISOString(),
|
|
321
|
+
};
|
|
322
|
+
registry.bindings[id] = record;
|
|
323
|
+
return record;
|
|
324
|
+
});
|
|
325
|
+
writeBindings(opts.homeDir, registry);
|
|
326
|
+
return records;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function listHookBindings(homeDir) {
|
|
330
|
+
return Object.values(readBindings(homeDir).bindings)
|
|
331
|
+
.sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)) || a.id.localeCompare(b.id));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function unbindHookBinding(id, options) {
|
|
335
|
+
const registry = readBindings(options && options.homeDir);
|
|
336
|
+
if (!registry.bindings[id]) return false;
|
|
337
|
+
delete registry.bindings[id];
|
|
338
|
+
writeBindings(options && options.homeDir, registry);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function createHookAdapter(options) {
|
|
343
|
+
const opts = options || {};
|
|
344
|
+
const host = opts.host || 'generic';
|
|
345
|
+
const capabilities = capabilitiesForHost(host);
|
|
346
|
+
return {
|
|
347
|
+
host,
|
|
348
|
+
capabilities: () => ({ ...capabilities }),
|
|
349
|
+
validate: (manifest) => validateHookRequirements(manifest),
|
|
350
|
+
evaluate: (event, manifest, context) => evaluateHook({
|
|
351
|
+
host,
|
|
352
|
+
event,
|
|
353
|
+
manifest,
|
|
354
|
+
context,
|
|
355
|
+
capabilities,
|
|
356
|
+
}),
|
|
357
|
+
bind: (manifest, event) => bindHookRequirements({
|
|
358
|
+
host,
|
|
359
|
+
homeDir: opts.homeDir,
|
|
360
|
+
manifest,
|
|
361
|
+
event,
|
|
362
|
+
}),
|
|
363
|
+
unbind: (id) => unbindHookBinding(id, { homeDir: opts.homeDir }),
|
|
364
|
+
listBindings: () => listHookBindings(opts.homeDir),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
module.exports = {
|
|
369
|
+
EVENTS,
|
|
370
|
+
CAPABILITY_LEVELS,
|
|
371
|
+
EVIDENCE_KINDS,
|
|
372
|
+
FALLBACKS,
|
|
373
|
+
CODEX_CAPABILITIES,
|
|
374
|
+
appendHookEvidence,
|
|
375
|
+
bindHookRequirements,
|
|
376
|
+
bindingId,
|
|
377
|
+
bindingsPath,
|
|
378
|
+
capabilitiesForHost,
|
|
379
|
+
createHookAdapter,
|
|
380
|
+
evaluateHook,
|
|
381
|
+
hookLogPath,
|
|
382
|
+
listHookBindings,
|
|
383
|
+
unbindHookBinding,
|
|
384
|
+
validateHookRequirements,
|
|
385
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function evidencePath(options) {
|
|
7
|
+
const home = (options && options.homeDir) || os.homedir();
|
|
8
|
+
return path.join(home, '.yottaskills', 'install-log.jsonl');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function appendEvidence(entry, options) {
|
|
12
|
+
const file = evidencePath(options);
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8');
|
|
15
|
+
return file;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { evidencePath, appendEvidence };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const manifestLib = require('./manifest');
|
|
5
|
+
const scanLib = require('./skills-scan');
|
|
6
|
+
|
|
7
|
+
function addCheck(checks, id, ok, message, severity, hint) {
|
|
8
|
+
checks.push({
|
|
9
|
+
id,
|
|
10
|
+
ok: !!ok,
|
|
11
|
+
severity: severity || (ok ? 'info' : 'error'),
|
|
12
|
+
message,
|
|
13
|
+
hint: hint || null,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readManifest(target, slug) {
|
|
18
|
+
const file = path.join(target, manifestLib.MANIFEST_FILE);
|
|
19
|
+
if (!fs.existsSync(file)) return { value: null, error: null };
|
|
20
|
+
try {
|
|
21
|
+
return { value: JSON.parse(fs.readFileSync(file, 'utf8')), error: null };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return { value: null, error: '无法解析 skill-manifest.json: ' + error.message };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 优先读取目标目录对应的副本版本,旧注册表缺少 variants 时回退聚合版本。 */
|
|
28
|
+
function registryVersionForTarget(record, target) {
|
|
29
|
+
const variants = Array.isArray(record.variants) ? record.variants : [];
|
|
30
|
+
const normalizedTarget = path.resolve(target);
|
|
31
|
+
const exact = variants.find((variant) => {
|
|
32
|
+
if (!variant || !variant.dir) return false;
|
|
33
|
+
return path.resolve(variant.dir) === normalizedTarget;
|
|
34
|
+
});
|
|
35
|
+
if (exact && exact.version) {
|
|
36
|
+
return { version: String(exact.version).trim(), scope: 'target' };
|
|
37
|
+
}
|
|
38
|
+
return { version: record.version ? String(record.version).trim() : '', scope: 'aggregate' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function checkInstalledSkill(context) {
|
|
42
|
+
const ctx = context || {};
|
|
43
|
+
const slug = String(ctx.slug || '');
|
|
44
|
+
const target = ctx.target ? path.resolve(ctx.target) : '';
|
|
45
|
+
const checks = [];
|
|
46
|
+
const errors = [];
|
|
47
|
+
const warnings = [];
|
|
48
|
+
const fixes = [];
|
|
49
|
+
let version = null;
|
|
50
|
+
|
|
51
|
+
if (!slug) {
|
|
52
|
+
addCheck(checks, 'slug', false, '缺少技能 slug');
|
|
53
|
+
errors.push('缺少技能 slug');
|
|
54
|
+
}
|
|
55
|
+
if (!target || !fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
56
|
+
addCheck(checks, 'target', false, '技能目录不存在' + (target ? ': ' + target : ''));
|
|
57
|
+
errors.push('技能目录不存在' + (target ? ': ' + target : ''));
|
|
58
|
+
return { ok: false, slug, version, checks, errors, warnings, fixes };
|
|
59
|
+
}
|
|
60
|
+
addCheck(checks, 'target', true, '技能目录存在');
|
|
61
|
+
|
|
62
|
+
const skillFile = path.join(target, 'SKILL.md');
|
|
63
|
+
let text = null;
|
|
64
|
+
try {
|
|
65
|
+
text = fs.readFileSync(skillFile, 'utf8');
|
|
66
|
+
addCheck(checks, 'skill_file', true, 'SKILL.md 可读');
|
|
67
|
+
} catch (error) {
|
|
68
|
+
addCheck(checks, 'skill_file', false, 'SKILL.md 不可读: ' + error.message);
|
|
69
|
+
errors.push('SKILL.md 不可读: ' + error.message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let frontmatter = null;
|
|
73
|
+
if (text !== null) {
|
|
74
|
+
frontmatter = scanLib.parseFrontmatter(text);
|
|
75
|
+
if (!frontmatter) {
|
|
76
|
+
addCheck(checks, 'frontmatter', false, 'SKILL.md 缺少有效 frontmatter');
|
|
77
|
+
errors.push('SKILL.md 缺少有效 frontmatter');
|
|
78
|
+
} else {
|
|
79
|
+
addCheck(checks, 'frontmatter', true, 'SKILL.md frontmatter 有效');
|
|
80
|
+
if (frontmatter.name !== slug) {
|
|
81
|
+
addCheck(checks, 'slug_identity', false, 'SKILL.md name 与目标 slug 不一致');
|
|
82
|
+
errors.push('SKILL.md name 与目标 slug 不一致');
|
|
83
|
+
} else {
|
|
84
|
+
addCheck(checks, 'slug_identity', true, 'SKILL.md slug 一致');
|
|
85
|
+
}
|
|
86
|
+
version = frontmatter.version ? String(frontmatter.version).trim() : null;
|
|
87
|
+
if (!version || !/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
88
|
+
addCheck(checks, 'version_format', false, 'SKILL.md 版本号非法');
|
|
89
|
+
errors.push('SKILL.md 版本号非法');
|
|
90
|
+
} else {
|
|
91
|
+
addCheck(checks, 'version_format', true, 'SKILL.md 版本号有效');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (ctx.expectedVersion && version && version !== ctx.expectedVersion) {
|
|
97
|
+
addCheck(checks, 'version_match', false, '版本不一致:已安装 v' + version + ',期望 v' + ctx.expectedVersion);
|
|
98
|
+
errors.push('版本不一致:已安装 v' + version + ',期望 v' + ctx.expectedVersion);
|
|
99
|
+
} else if (ctx.expectedVersion && version) {
|
|
100
|
+
addCheck(checks, 'version_match', true, '版本与期望一致');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const loadedManifest = readManifest(target, slug);
|
|
104
|
+
if (loadedManifest.error) {
|
|
105
|
+
addCheck(checks, 'manifest', false, loadedManifest.error);
|
|
106
|
+
errors.push(loadedManifest.error);
|
|
107
|
+
} else if (loadedManifest.value) {
|
|
108
|
+
if (loadedManifest.value.trust !== 'yottameta') {
|
|
109
|
+
addCheck(checks, 'manifest_trust', false, 'manifest trust 必须为 yottameta');
|
|
110
|
+
errors.push('manifest trust 必须为 yottameta');
|
|
111
|
+
} else {
|
|
112
|
+
addCheck(checks, 'manifest_trust', true, 'manifest trust 为 yottameta');
|
|
113
|
+
}
|
|
114
|
+
const validation = manifestLib.validateManifest(loadedManifest.value, {
|
|
115
|
+
skill: {
|
|
116
|
+
slug,
|
|
117
|
+
pkg: ctx.expectedPackage || loadedManifest.value.package,
|
|
118
|
+
version: version || loadedManifest.value.version,
|
|
119
|
+
},
|
|
120
|
+
pkgDir: target,
|
|
121
|
+
requirePackageJson: false,
|
|
122
|
+
});
|
|
123
|
+
if (!validation.ok) {
|
|
124
|
+
addCheck(checks, 'manifest', false, 'manifest 校验失败: ' + validation.errors.join('; '));
|
|
125
|
+
errors.push.apply(errors, validation.errors);
|
|
126
|
+
} else {
|
|
127
|
+
addCheck(checks, 'manifest', true, 'manifest 校验通过');
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
addCheck(checks, 'manifest', true, '未提供 skill-manifest.json,使用家族默认契约');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ctx.registry) {
|
|
134
|
+
const record = ctx.registry.skills && ctx.registry.skills[slug];
|
|
135
|
+
if (!record || record.status === 'gone') {
|
|
136
|
+
addCheck(checks, 'registry', false, '注册表未记录当前技能', 'warning');
|
|
137
|
+
warnings.push('注册表未记录当前技能');
|
|
138
|
+
fixes.push('运行 yotta-skills --reindex 重扫注册表');
|
|
139
|
+
} else {
|
|
140
|
+
const registryVersion = registryVersionForTarget(record, target);
|
|
141
|
+
if (version && registryVersion.version && registryVersion.version !== version) {
|
|
142
|
+
const scopeText = registryVersion.scope === 'target' ? '注册表版本(目标副本)' : '注册表版本(聚合)';
|
|
143
|
+
addCheck(
|
|
144
|
+
checks,
|
|
145
|
+
'registry',
|
|
146
|
+
false,
|
|
147
|
+
scopeText + ' v' + registryVersion.version + ' 与已安装 v' + version + ' 不一致',
|
|
148
|
+
'warning',
|
|
149
|
+
);
|
|
150
|
+
warnings.push(scopeText + ' v' + registryVersion.version + ' 与已安装 v' + version + ' 不一致');
|
|
151
|
+
fixes.push('运行 yotta-skills --reindex 重扫注册表');
|
|
152
|
+
} else {
|
|
153
|
+
addCheck(checks, 'registry', true, '注册表记录一致');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
ok: errors.length === 0,
|
|
160
|
+
slug,
|
|
161
|
+
version,
|
|
162
|
+
checks,
|
|
163
|
+
errors,
|
|
164
|
+
warnings,
|
|
165
|
+
fixes: Array.from(new Set(fixes)),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { checkInstalledSkill };
|