@yottameta/yotta-skills 0.19.28 → 0.20.1
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/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/SKILL.md +8 -1
- package/bin/yotta-skills.js +129 -15
- package/install.sh +1 -1
- package/lib/install-pipeline.js +7 -1
- package/lib/provider.js +248 -0
- package/lib/scan-policy.js +209 -0
- package/lib/verify-gate.js +65 -11
- package/package.json +2 -1
- package/references/install-flow.md +3 -0
- package/references/provider-protocol.md +101 -0
- package/references/skill-list.md +1 -1
- package/scan-policy.json +525 -0
- package/scripts/yotta-skills-mcp.py +1 -1
- package/skill-manifest.json +1 -1
- package/skills.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## v0.20.1 (2026-09-29)
|
|
2
|
+
- 家族安装接入 scanPolicy 例外复核:元信判 `DO NOT INSTALL` 时,按包内 `scan-policy.json` 的已审查例外(逐条绑定技能 + 版本 + treeHash + 规则 + 路径)复核,豁免检测规则表 / 攻防样例 / 文档说明类命中;复核后无阻断级发现则继续安装。
|
|
3
|
+
- 修复元忆 / 元察 / 元鉴从元阁安装时被元信自指误报阻断的问题(检测类技能自带规则表字面量);例外表与 handoff / OpenCode 的 scanPolicy 同源,由 `tools/build_scan_policy.py` 生成。
|
|
4
|
+
- 技能升版或内容变化后旧例外自动失效(treeHash 绑定,fail-closed),安装回到元信原始判定。
|
|
5
|
+
- 安装日志新增 `scan_policy` 字段(是否应用 / 豁免条数 / version / treeHash)。
|
|
6
|
+
- 元信扫描输出解析失败(空输出 / 截断 / 子进程抖动)自动重试一次;仍失败时报错带退出码与 stderr 摘要。
|
|
7
|
+
- 静态 playbook / 路由 / hook 契约零变更。
|
|
8
|
+
|
|
9
|
+
## v0.20.0 (2026-09-28)
|
|
10
|
+
- 新增 `o1.route` 动态扩展口:`--route` / MCP `route_request` 可选调用用户配置的本地扩展提供方(provider),在已装注册表白名单内增补 / 重排路由结果。
|
|
11
|
+
- 静态 playbook 结果先算必算;未配置或未授权 / 超时 / 非法输出时一律回落静态结果,文本输出与历史一致,`--json` 增加 `dynamic` 状态块。
|
|
12
|
+
- 清单同步:元忆 pin 0.19.0。
|
|
13
|
+
- 协议与配置见 `references/provider-protocol.md`。
|
|
14
|
+
|
|
1
15
|
## v0.19.28 (2026-09-28)
|
|
2
16
|
|
|
3
17
|
清单同步元盾 `0.1.7`(代码围栏占位符修复 20 处 / 非法转义告警与致命异常退出码 fail-open 修复 / 彩色输出路径在真人终端崩溃修复 / 新增宿主接入说明)。
|
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Installing the whole YottaMeta family used to mean running <code>npx</code> for
|
|
|
26
26
|
元阁 is also the **family orchestration planner**: given a task, check the orchestration table first — which skills to combine, in what order, and why — then install exactly those. The decision table ships in the package as <code>references/orchestration.md</code> and is summarized in <code>SKILL.md</code>.
|
|
27
27
|
|
|
28
28
|
- **List** - see the whole family: slug, Chinese name, package, version and a one-line description.
|
|
29
|
-
- **Orchestration routing** - <code>--route</code> turns a task summary into a candidate combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install command; it only suggests installation and never installs automatically.
|
|
29
|
+
- **Orchestration routing** - <code>--route</code> turns a task summary into a candidate combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install command; it only suggests installation and never installs automatically. An optional local provider may add or re-order installed skills within the allow-list (no behavior change when unconfigured; see <code>references/provider-protocol.md</code>).
|
|
30
30
|
- **Install** — install everything (or a subset) into an agent's default user-level directory or any directory.
|
|
31
31
|
- **Update** — incremental update: add missing skills, upgrade version-skewed ones.
|
|
32
32
|
- **Update check / auto-update** — <code>update --check</code> compares installed skill versions against the npm registry and reports; <code>update --check --scheduled</code> is the weekly background entry with a local cache and jitter, so it stays offline until due; <code>update --auto</code> upgrades the installed YottaMeta family to the latest (non-family skills are never auto-updated).
|
|
@@ -110,7 +110,7 @@ Requirements: Node.js 18+, npm, and system <code>tar</code> (built into Windows
|
|
|
110
110
|
| `--registry <url>` | npm registry to check against (default https://registry.npmjs.org/; `YOTTA_SKILLS_REGISTRY` overrides) |
|
|
111
111
|
| `--inventory` | Inventory installed skills: scan skill directories and update the local registry (self-contained); `--json` for JSON, `--project` adds project-level dirs |
|
|
112
112
|
| `--reindex` | Re-index the registry: re-scan skill directories and merge changes incrementally (on demand; `install` / `update` re-index automatically; `--rescan` is a synonym); `--json` for JSON |
|
|
113
|
-
| `--route <task-summary>` | Static orchestration routing: return a combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install suggestion; also lists other installed (non-YottaMeta) skills as candidates matched mechanically against their frontmatter description, tagged "not scanned", read-only and never auto-invoked; `--json` for JSON, `--project` adds project-level dirs |
|
|
113
|
+
| `--route <task-summary>` | Static orchestration routing: return a combination, call order, per-skill roles, confidence, evidence, installed/missing status, and an install suggestion; also lists other installed (non-YottaMeta) skills as candidates matched mechanically against their frontmatter description, tagged "not scanned", read-only and never auto-invoked; an optional local provider may add or re-order installed skills within the allow-list (see <code>references/provider-protocol.md</code>); `--json` for JSON plus a `dynamic` status block, `--project` adds project-level dirs |
|
|
114
114
|
| `--no-reindex` | Do not re-index the registry automatically after `install` / `update` |
|
|
115
115
|
| `--dry-run` | Preview the install / update list; no network, no changes |
|
|
116
116
|
| `--pin` | Lock the exact manifest versions (default) |
|
package/README.zh-CN.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
元阁也是全家的**编排策划层**:接到需求先查「编排策划」组合表——该组合哪几个技能、按什么顺序、各自强在哪——然后给出安装与调用**建议**,由用户确认后执行。决策表随包提供(<code>references/orchestration.md</code>),<code>SKILL.md</code> 中有摘要。
|
|
30
30
|
|
|
31
31
|
- **看清单**--全家技能一览:slug / 中文名 / 包名 / 版本 / 一句话说明。
|
|
32
|
-
- **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description
|
|
32
|
+
- **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。可选本地扩展提供方可在已装白名单内增补 / 重排(未配置时行为不变),协议见 `references/provider-protocol.md`。
|
|
33
33
|
- **安装**——装全家(或指定技能)到智能体默认用户级目录或任意目录。
|
|
34
34
|
- **更新**——增量更新:补齐缺失技能、升级版本不一致的技能。
|
|
35
35
|
- **更新检查 / 自动更新**——<code>update --check</code> 只读比对已装技能版本与 npm 注册表并报告;<code>update --check --scheduled</code> 是后台周检入口,用本地缓存和随机抖动做到未到期不联网;<code>update --auto</code> 检查后自动把已装元阁家族升到最新(非元阁家族技能绝不自动更新)。
|
|
@@ -113,7 +113,7 @@ npx -y @yottameta/yotta-skills --reindex
|
|
|
113
113
|
| `--registry <url>` | 检查的 npm registry 地址(默认 https://registry.npmjs.org/;`YOTTA_SKILLS_REGISTRY` 覆盖) |
|
|
114
114
|
| `--inventory` | 盘点已装技能:扫描技能目录并更新本地注册表(自包含);`--json` 输出 JSON、`--project` 附扫项目级目录 |
|
|
115
115
|
| `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(install / update 完成后自动重扫,也可随时手动运行;`--rescan` 同义);`--json` 输出 JSON |
|
|
116
|
-
| `--route <需求摘要>` |
|
|
116
|
+
| `--route <需求摘要>` | 静态编排路由:输出组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装建议;可选本地扩展提供方在已装白名单内增补 / 重排(未配置时行为不变,协议见 `references/provider-protocol.md`);`--json` 输出 JSON 与 `dynamic` 状态块、`--project` 附扫项目级目录 |
|
|
117
117
|
| `--no-reindex` | 安装 / 更新后不自动重扫注册表 |
|
|
118
118
|
| `--dry-run` | 预览将执行的安装 / 更新清单;不联网、不改动 |
|
|
119
119
|
| `--pin` | 锁死清单精确版本(默认) |
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-skills
|
|
3
|
-
version: 0.
|
|
3
|
+
version: 0.20.1
|
|
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:
|
|
@@ -43,6 +43,7 @@ metadata:
|
|
|
43
43
|
**元阁全家的总编排策划 + 编排路由 + 一键安装 + 技能盘点**,一句话四层:
|
|
44
44
|
|
|
45
45
|
- **路由层(该用哪几个)**:`--route "<需求摘要>"` / MCP `route_request` 查本地注册表与静态编排 playbook,输出候选组合、调用顺序、每个技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。
|
|
46
|
+
- **动态扩展口(v0.20.0,capability `o1.route`)**:可选调用用户显式配置的本地扩展提供方(provider),在已装注册表白名单内对路由结果增补 / 重排;静态 playbook 结果先算必算、永不缺席,未配置或调用失败时文本输出与历史一致,`--json` 仅多一个 `dynamic` 状态块。协议与配置见 `references/provider-protocol.md`。
|
|
46
47
|
- **策划层(怎么用)**:接到需求,先按「编排策划」定位命中哪个组合——哪些元技能搭配起来最强、适合什么场景、AI 该给出什么安装与调用建议(**安装与调用均由用户确认后执行**)。
|
|
47
48
|
- **安装层(怎么装)**:一条 `npx -y @yottameta/yotta-skills` 把组合/全家装进指定智能体或目录——`--list` 看清单、`install` 装、`update` 增量更新、`doctor` 只读自检、`rollback` 恢复快照、`--dry-run` 预览、`--pin` 锁版本。
|
|
48
49
|
|
|
@@ -347,6 +348,12 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
|
|
|
347
348
|
- `INSTALL WITH CAUTION` / `REVIEW REQUIRED`:继续安装,但显示风险并写入证据;
|
|
348
349
|
- `DO NOT INSTALL` 或扫描失败:阻断,不替换旧版本。
|
|
349
350
|
|
|
351
|
+
扫描后元阁会用包内 `scan-policy.json`(已审查例外表)复核:每条例外绑定「技能 + 版本 +
|
|
352
|
+
内容 treeHash + 规则 + 路径」,只豁免检测规则表、攻防样例与文档说明这类已审查命中。
|
|
353
|
+
技能升版或内容有任何变化,旧例外自动失效(fail-closed),安装回到元信原始判定。
|
|
354
|
+
命中例外的安装会显示 `↳ scanPolicy 复核:豁免 N 条已审查发现` 并写入安装证据;
|
|
355
|
+
未命中例外的 `DO NOT INSTALL` 照常阻断。
|
|
356
|
+
|
|
350
357
|
引擎查找顺序(v0.19.13 收紧):`--verify` 指定路径 → 环境变量 `YOTTA_SKILLS_VERIFY` →
|
|
351
358
|
**受信安装记录**(`~/.yottaskills/trusted-verifier.json`,由安装管线在元信安装 / 更新成功后写入,
|
|
352
359
|
记录包身份 + 引擎路径 + SHA-256)。候选引擎必须同时满足:路径不经符号链接跳转、
|
package/bin/yotta-skills.js
CHANGED
|
@@ -35,10 +35,11 @@ const lifecycleLib = require('../lib/install-lifecycle');
|
|
|
35
35
|
const snapshotLib = require('../lib/install-snapshot');
|
|
36
36
|
const updateCheckLib = require('../lib/update-check');
|
|
37
37
|
const hookAdapterLib = require('../lib/hook-adapter');
|
|
38
|
+
const scanPolicyLib = require('../lib/scan-policy');
|
|
38
39
|
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
39
40
|
|
|
40
41
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
41
|
-
let VERSION = '0.
|
|
42
|
+
let VERSION = '0.20.1';
|
|
42
43
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
43
44
|
|
|
44
45
|
function loadManifest() {
|
|
@@ -586,22 +587,33 @@ function runScan(engine, skillDir) {
|
|
|
586
587
|
return gateLib.runVerifier(engine, skillDir, { python, spawnSync });
|
|
587
588
|
}
|
|
588
589
|
|
|
589
|
-
function scanTarget(engine, skillDir) {
|
|
590
|
+
function scanTarget(engine, skillDir, context) {
|
|
590
591
|
const scan = runScan(engine, skillDir);
|
|
591
|
-
if (scan.ok) {
|
|
592
|
-
const counts = scan.counts || {};
|
|
593
|
-
const line = 'critical ' + (counts.critical || 0) + ' / high ' + (counts.high || 0) +
|
|
594
|
-
' / medium ' + (counts.medium || 0) + ' / low ' + (counts.low || 0) +
|
|
595
|
-
' / info ' + (counts.info || 0);
|
|
596
|
-
out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
|
|
597
|
-
if (scan.verdict === gateLib.BLOCK) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,已阻断安装。');
|
|
598
|
-
else if (scan.verdict === gateLib.CAUTION || scan.verdict === gateLib.REVIEW) {
|
|
599
|
-
out(' ⚠ 元信 verdict 为 ' + scan.verdict + ',继续安装并保留风险证据。');
|
|
600
|
-
}
|
|
601
|
-
} else {
|
|
592
|
+
if (!scan.ok) {
|
|
602
593
|
out(' 元信 scan: ' + scan.error);
|
|
603
|
-
|
|
604
|
-
|
|
594
|
+
return scan;
|
|
595
|
+
}
|
|
596
|
+
const ctx = context || {};
|
|
597
|
+
const reviewed = scanPolicyLib.applyScanPolicy(scan, {
|
|
598
|
+
slug: ctx.slug,
|
|
599
|
+
version: ctx.version,
|
|
600
|
+
pkgDir: skillDir,
|
|
601
|
+
policy: scanPolicyLib.loadPolicy(path.join(PKG_ROOT, 'scan-policy.json')),
|
|
602
|
+
});
|
|
603
|
+
const counts = scan.counts || {};
|
|
604
|
+
const line = 'critical ' + (counts.critical || 0) + ' / high ' + (counts.high || 0) +
|
|
605
|
+
' / medium ' + (counts.medium || 0) + ' / low ' + (counts.low || 0) +
|
|
606
|
+
' / info ' + (counts.info || 0);
|
|
607
|
+
out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
|
|
608
|
+
if (reviewed.policy && reviewed.policy.applied && reviewed.policy.excluded > 0) {
|
|
609
|
+
out(' ↳ scanPolicy 复核:豁免 ' + reviewed.policy.excluded +
|
|
610
|
+
' 条已审查发现(检测规则 / 文档说明,treeHash 绑定)→ ' + reviewed.verdict);
|
|
611
|
+
}
|
|
612
|
+
if (reviewed.verdict === gateLib.BLOCK) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,已阻断安装。');
|
|
613
|
+
else if (reviewed.verdict === gateLib.CAUTION || reviewed.verdict === gateLib.REVIEW) {
|
|
614
|
+
out(' ⚠ 元信 verdict 为 ' + reviewed.verdict + ',继续安装并保留风险证据。');
|
|
615
|
+
}
|
|
616
|
+
return reviewed;
|
|
605
617
|
}
|
|
606
618
|
|
|
607
619
|
// ── 安装 ───────────────────────────────────────────────────────────────────
|
|
@@ -1202,12 +1214,110 @@ function runReindex(opts) {
|
|
|
1202
1214
|
out('注册表: ' + scan.registryPath());
|
|
1203
1215
|
}
|
|
1204
1216
|
|
|
1217
|
+
function dynamicBlock(status, providerId, note) {
|
|
1218
|
+
return {
|
|
1219
|
+
status: status || 'not_installed',
|
|
1220
|
+
provider_id: providerId || '',
|
|
1221
|
+
applied: false,
|
|
1222
|
+
added: [],
|
|
1223
|
+
dropped: [],
|
|
1224
|
+
note: note || '',
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* o1.route 动态扩展口:静态结果先算必算;provider 只允许在已装注册表白名单内增补 / 重排,
|
|
1230
|
+
* 任何失败都回静态结果(fail-open)。协议见 references/provider-protocol.md。
|
|
1231
|
+
*/
|
|
1232
|
+
function applyDynamicRoute(result, registry, opts) {
|
|
1233
|
+
const provider = require('../lib/provider');
|
|
1234
|
+
let run;
|
|
1235
|
+
try {
|
|
1236
|
+
run = provider.runCapability('o1.route', {
|
|
1237
|
+
request: String(opts.route || ''),
|
|
1238
|
+
static_result: {
|
|
1239
|
+
playbook: result.playbook.id,
|
|
1240
|
+
confidence: result.confidence,
|
|
1241
|
+
skills: result.skills.map((skill) => skill.slug),
|
|
1242
|
+
},
|
|
1243
|
+
installed_skills: Object.values(registry.skills || {})
|
|
1244
|
+
.filter((record) => record && record.status !== 'gone')
|
|
1245
|
+
.map((record) => ({ slug: record.slug, version: record.version || '', sources: record.sources || [] })),
|
|
1246
|
+
});
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
return dynamicBlock('error', '', '动态路由装载失败:' + e.message);
|
|
1249
|
+
}
|
|
1250
|
+
const block = dynamicBlock(run.status, run.provider_id, run.note || run.message || '');
|
|
1251
|
+
if (run.status !== 'active' || !run.data || typeof run.data !== 'object') return block;
|
|
1252
|
+
|
|
1253
|
+
const installed = new Set(Object.values(registry.skills || {})
|
|
1254
|
+
.filter((record) => record && record.status !== 'gone')
|
|
1255
|
+
.map((record) => record.slug));
|
|
1256
|
+
const requested = Array.isArray(run.data.skills) ? run.data.skills : [];
|
|
1257
|
+
const seen = new Set();
|
|
1258
|
+
const ordered = [];
|
|
1259
|
+
for (const item of requested) {
|
|
1260
|
+
const slug = item && typeof item === 'object' ? String(item.slug || '') : '';
|
|
1261
|
+
if (!slug) continue;
|
|
1262
|
+
if (seen.has(slug) || !installed.has(slug)) {
|
|
1263
|
+
block.dropped.push(slug);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
seen.add(slug);
|
|
1267
|
+
ordered.push({ slug, role: typeof item.role === 'string' ? item.role : '' });
|
|
1268
|
+
}
|
|
1269
|
+
if (!ordered.length) return block;
|
|
1270
|
+
|
|
1271
|
+
const staticBySlug = new Map(result.skills.map((skill) => [skill.slug, skill]));
|
|
1272
|
+
const next = [];
|
|
1273
|
+
for (const item of ordered) {
|
|
1274
|
+
const existing = staticBySlug.get(item.slug);
|
|
1275
|
+
if (existing) {
|
|
1276
|
+
next.push(existing);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const record = registry.skills[item.slug] || {};
|
|
1280
|
+
next.push({
|
|
1281
|
+
slug: item.slug,
|
|
1282
|
+
order: 0,
|
|
1283
|
+
role: item.role || '由扩展提供方补充',
|
|
1284
|
+
installed: true,
|
|
1285
|
+
version: record.version || '',
|
|
1286
|
+
sources: record.sources || [],
|
|
1287
|
+
variants: Array.isArray(record.variants) ? record.variants : [],
|
|
1288
|
+
conflicts: Array.isArray(record.conflicts) ? record.conflicts : [],
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
for (const skill of result.skills) {
|
|
1292
|
+
if (!next.some((item) => item.slug === skill.slug)) next.push(skill);
|
|
1293
|
+
}
|
|
1294
|
+
next.forEach((skill, index) => { skill.order = index + 1; });
|
|
1295
|
+
block.added = next.filter((skill) => !staticBySlug.has(skill.slug)).map((skill) => skill.slug);
|
|
1296
|
+
block.applied = true;
|
|
1297
|
+
result.skills = next;
|
|
1298
|
+
return block;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function dynamicRouteText(block) {
|
|
1302
|
+
if (!block) return '';
|
|
1303
|
+
if (block.status === 'active') {
|
|
1304
|
+
const who = block.provider_id ? '提供方 ' + block.provider_id : '提供方';
|
|
1305
|
+
return '已应用(' + who + ';新增 ' + block.added.length + ' / 丢弃 ' + block.dropped.length + ')';
|
|
1306
|
+
}
|
|
1307
|
+
if (block.status === 'license_required') return '需授权(该能力需要授权后使用;静态路由不受影响)';
|
|
1308
|
+
if (block.status === 'timeout') return '未生效(提供方超时,已回落静态路由)';
|
|
1309
|
+
if (block.status === 'invalid_output') return '未生效(提供方输出无效,已回落静态路由)';
|
|
1310
|
+
if (block.status === 'error') return '未生效(提供方异常,已回落静态路由)';
|
|
1311
|
+
return block.status;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1205
1314
|
/** --route:静态编排路由,输出组合、顺序、角色、缺失技能建议与其他已装技能候选。 */
|
|
1206
1315
|
function runRoute(opts) {
|
|
1207
1316
|
const { routeRequest, defaultYottaSlugs } = require('../lib/route');
|
|
1208
1317
|
const { registry } = reindexRegistry(opts);
|
|
1209
1318
|
const yottaSlugs = new Set([...defaultYottaSlugs(), ...MANIFEST.map((s) => s.slug)]);
|
|
1210
1319
|
const result = routeRequest(opts.route, { registry, yottaSlugs });
|
|
1320
|
+
result.dynamic = applyDynamicRoute(result, registry, opts);
|
|
1211
1321
|
if (opts.json) {
|
|
1212
1322
|
out(JSON.stringify(result, null, 2));
|
|
1213
1323
|
return;
|
|
@@ -1244,6 +1354,10 @@ function runRoute(opts) {
|
|
|
1244
1354
|
}
|
|
1245
1355
|
out('安全提示: 其他已装技能只读 frontmatter description 做机械匹配,不读取全文指令、不自动调用;使用/安装前请先执行装前安全扫描,决定权在用户。');
|
|
1246
1356
|
}
|
|
1357
|
+
if (result.dynamic.status !== 'not_installed') {
|
|
1358
|
+
out('');
|
|
1359
|
+
out('动态路由: ' + dynamicRouteText(result.dynamic));
|
|
1360
|
+
}
|
|
1247
1361
|
out('');
|
|
1248
1362
|
out('应用模式: 显式调用(可经用户确认后切换为按场景自动调用)');
|
|
1249
1363
|
out('说明: ' + result.disclaimer);
|
package/install.sh
CHANGED
|
@@ -89,7 +89,7 @@ install_to() {
|
|
|
89
89
|
rm -rf -- "$dest/$item"
|
|
90
90
|
fi
|
|
91
91
|
done
|
|
92
|
-
for item in SKILL.md skill-manifest.json references scripts assets skills.json LICENSE NOTICE README.md README.zh-CN.md CHANGELOG.md USER_GUIDE.md; do
|
|
92
|
+
for item in SKILL.md skill-manifest.json references scripts assets skills.json scan-policy.json LICENSE NOTICE README.md README.zh-CN.md CHANGELOG.md USER_GUIDE.md; do
|
|
93
93
|
if [ -e "$SOURCE_DIR/$item" ]; then
|
|
94
94
|
cp -RP "$SOURCE_DIR/$item" "$dest/"
|
|
95
95
|
fi
|
package/lib/install-pipeline.js
CHANGED
|
@@ -148,7 +148,10 @@ function createInstaller(deps) {
|
|
|
148
148
|
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'block', error: gate.error });
|
|
149
149
|
return { skill, status: 'fail', version: packed.resolved, note: gate.error, exitCode: 5 };
|
|
150
150
|
}
|
|
151
|
-
const scanResult = deps.scanTarget(gate.engine, extracted.pkgDir
|
|
151
|
+
const scanResult = deps.scanTarget(gate.engine, extracted.pkgDir, {
|
|
152
|
+
slug: skill.slug,
|
|
153
|
+
version: packed.resolved,
|
|
154
|
+
});
|
|
152
155
|
if (!scanResult.ok) {
|
|
153
156
|
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'block', error: scanResult.error });
|
|
154
157
|
return { skill, status: 'fail', version: packed.resolved, note: scanResult.error, exitCode: 5 };
|
|
@@ -193,6 +196,7 @@ function createInstaller(deps) {
|
|
|
193
196
|
decision: 'block',
|
|
194
197
|
verdict: scanResult.verdict,
|
|
195
198
|
counts: scanResult.counts,
|
|
199
|
+
scan_policy: scanResult.policy || null,
|
|
196
200
|
hook_decision: hookEvaluation.decision,
|
|
197
201
|
});
|
|
198
202
|
return {
|
|
@@ -208,6 +212,7 @@ function createInstaller(deps) {
|
|
|
208
212
|
verdict: scanResult.verdict,
|
|
209
213
|
counts: scanResult.counts,
|
|
210
214
|
warn: verdict.warn,
|
|
215
|
+
policy: scanResult.policy || null,
|
|
211
216
|
};
|
|
212
217
|
}
|
|
213
218
|
|
|
@@ -332,6 +337,7 @@ function createInstaller(deps) {
|
|
|
332
337
|
bootstrap_scan: gate.mode === 'trusted-bootstrap' && skill.slug === 'yotta-verify' ? 'self' : null,
|
|
333
338
|
verdict: scan.verdict,
|
|
334
339
|
decision: scan.decision,
|
|
340
|
+
scan_policy: scan.policy || null,
|
|
335
341
|
snapshot,
|
|
336
342
|
});
|
|
337
343
|
} catch (error) {
|
package/lib/provider.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 扩展提供方(provider)装载器 v1:外部子进程 + stdin/stdout JSON。
|
|
4
|
+
// 设计:docs/路线B-P1-接口预留设计-2026-09-28.md(根仓库);对外协议:references/provider-protocol.md。
|
|
5
|
+
// 零依赖;未配置 / 任何失败都 fail-open 回开源基线,不阻断调用方。
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const SCHEMA = 1;
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 600;
|
|
15
|
+
const MAX_TIMEOUT_MS = 5000;
|
|
16
|
+
const MIN_TIMEOUT_MS = 50;
|
|
17
|
+
const MAX_STDOUT_BYTES = 256 * 1024;
|
|
18
|
+
const KNOWN_CAPABILITIES = ['o1.route', 'm1.adjudicate', 'memory.hook', 'context.paging'];
|
|
19
|
+
|
|
20
|
+
function providerHome() {
|
|
21
|
+
const fromEnv = process.env.YOTTA_PROVIDER_HOME;
|
|
22
|
+
if (fromEnv && String(fromEnv).trim()) return path.resolve(String(fromEnv).trim());
|
|
23
|
+
return path.join(os.homedir(), '.yottameta');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function configPath() {
|
|
27
|
+
return path.join(providerHome(), 'provider.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateProvider(item) {
|
|
31
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return 'provider 条目必须是对象';
|
|
32
|
+
if (!item.id || typeof item.id !== 'string') return 'provider 缺 id';
|
|
33
|
+
if (!Array.isArray(item.capabilities) || !item.capabilities.length) return 'provider ' + item.id + ' 缺 capabilities';
|
|
34
|
+
for (const capability of item.capabilities) {
|
|
35
|
+
if (KNOWN_CAPABILITIES.indexOf(capability) === -1) return 'provider ' + item.id + ' 含未知 capability:' + capability;
|
|
36
|
+
}
|
|
37
|
+
if (!Array.isArray(item.command) || !item.command.length) return 'provider ' + item.id + ' 的 command 必须是数组';
|
|
38
|
+
for (const part of item.command) {
|
|
39
|
+
if (typeof part !== 'string' || !part) return 'provider ' + item.id + ' 的 command 含非法参数';
|
|
40
|
+
if (part.indexOf('\0') !== -1) return 'provider ' + item.id + ' 的 command 含非法字符';
|
|
41
|
+
}
|
|
42
|
+
if (item.timeout_ms !== undefined && (!Number.isFinite(item.timeout_ms) || item.timeout_ms <= 0)) {
|
|
43
|
+
return 'provider ' + item.id + ' 的 timeout_ms 非法';
|
|
44
|
+
}
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeProvider(item) {
|
|
49
|
+
return {
|
|
50
|
+
id: item.id,
|
|
51
|
+
version: typeof item.version === 'string' ? item.version : '',
|
|
52
|
+
capabilities: item.capabilities.slice(),
|
|
53
|
+
command: item.command.slice(),
|
|
54
|
+
timeout_ms: Math.min(MAX_TIMEOUT_MS, Math.max(MIN_TIMEOUT_MS, Math.round(item.timeout_ms || DEFAULT_TIMEOUT_MS))),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readConfig() {
|
|
59
|
+
const file = configPath();
|
|
60
|
+
if (!fs.existsSync(file)) return { status: 'not_installed', providers: [], error: '' };
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { status: 'error', providers: [], error: 'provider.json 解析失败:' + e.message };
|
|
66
|
+
}
|
|
67
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
68
|
+
return { status: 'error', providers: [], error: 'provider.json 必须是对象' };
|
|
69
|
+
}
|
|
70
|
+
if (raw.schema !== SCHEMA) {
|
|
71
|
+
return { status: 'error', providers: [], error: 'provider.json schema 不支持:' + String(raw.schema) };
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(raw.providers)) {
|
|
74
|
+
return { status: 'error', providers: [], error: 'provider.json providers 必须是数组' };
|
|
75
|
+
}
|
|
76
|
+
const providers = [];
|
|
77
|
+
for (const item of raw.providers) {
|
|
78
|
+
const invalid = validateProvider(item);
|
|
79
|
+
if (invalid) return { status: 'error', providers: [], error: invalid };
|
|
80
|
+
providers.push(normalizeProvider(item));
|
|
81
|
+
}
|
|
82
|
+
return { status: 'ok', providers, error: '' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pickProvider(config, capability) {
|
|
86
|
+
if (!config || config.status !== 'ok') return null;
|
|
87
|
+
for (const item of config.providers) {
|
|
88
|
+
if (item.capabilities.indexOf(capability) !== -1) return item;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function minimalEnv() {
|
|
94
|
+
const keep = [
|
|
95
|
+
'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'COMSPEC', 'ComSpec',
|
|
96
|
+
'TEMP', 'TMP', 'HOME', 'USERPROFILE', 'LANG', 'LC_ALL',
|
|
97
|
+
];
|
|
98
|
+
const env = {};
|
|
99
|
+
for (const key of keep) {
|
|
100
|
+
if (process.env[key] !== undefined) env[key] = process.env[key];
|
|
101
|
+
}
|
|
102
|
+
env.YOTTA_PROVIDER_HOME = providerHome();
|
|
103
|
+
return env;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requestId() {
|
|
107
|
+
try {
|
|
108
|
+
if (typeof crypto.randomUUID === 'function') return crypto.randomUUID();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
// 回退到随机 hex
|
|
111
|
+
}
|
|
112
|
+
return crypto.randomBytes(16).toString('hex');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function invokeProvider(provider, capability, payload) {
|
|
116
|
+
const started = Date.now();
|
|
117
|
+
const request = {
|
|
118
|
+
schema: SCHEMA,
|
|
119
|
+
capability,
|
|
120
|
+
request_id: requestId(),
|
|
121
|
+
payload: payload || {},
|
|
122
|
+
};
|
|
123
|
+
let proc;
|
|
124
|
+
try {
|
|
125
|
+
proc = spawnSync(provider.command[0], provider.command.slice(1), {
|
|
126
|
+
input: JSON.stringify(request),
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
timeout: provider.timeout_ms,
|
|
129
|
+
maxBuffer: MAX_STDOUT_BYTES,
|
|
130
|
+
shell: false,
|
|
131
|
+
cwd: providerHome(),
|
|
132
|
+
env: minimalEnv(),
|
|
133
|
+
windowsHide: true,
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return {
|
|
137
|
+
status: 'error',
|
|
138
|
+
provider_id: provider.id,
|
|
139
|
+
duration_ms: Date.now() - started,
|
|
140
|
+
note: 'provider 启动失败:' + e.message,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const duration = Date.now() - started;
|
|
144
|
+
if (proc.error) {
|
|
145
|
+
const code = proc.error.code || '';
|
|
146
|
+
const timedOut = code === 'ETIMEDOUT';
|
|
147
|
+
const oversize = code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
|
|
148
|
+
return {
|
|
149
|
+
status: timedOut ? 'timeout' : 'error',
|
|
150
|
+
provider_id: provider.id,
|
|
151
|
+
duration_ms: duration,
|
|
152
|
+
note: timedOut
|
|
153
|
+
? 'provider 超时'
|
|
154
|
+
: (oversize
|
|
155
|
+
? 'provider 输出超过 ' + MAX_STDOUT_BYTES + ' 字节'
|
|
156
|
+
: 'provider 执行失败:' + (code || proc.error.message || 'unknown')),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (proc.status !== 0) {
|
|
160
|
+
return {
|
|
161
|
+
status: 'error',
|
|
162
|
+
provider_id: provider.id,
|
|
163
|
+
duration_ms: duration,
|
|
164
|
+
note: 'provider 退出码非 0:' + proc.status,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
let response;
|
|
168
|
+
try {
|
|
169
|
+
response = JSON.parse(String(proc.stdout || '').trim() || '{}');
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是 JSON' };
|
|
172
|
+
}
|
|
173
|
+
if (!response || typeof response !== 'object' || Array.isArray(response)) {
|
|
174
|
+
return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 输出不是对象' };
|
|
175
|
+
}
|
|
176
|
+
if (response.ok === false) {
|
|
177
|
+
const code = typeof response.code === 'string' && response.code ? response.code : 'provider_error';
|
|
178
|
+
return {
|
|
179
|
+
status: code === 'license_required' ? 'license_required' : 'error',
|
|
180
|
+
provider_id: provider.id,
|
|
181
|
+
code,
|
|
182
|
+
message: typeof response.message === 'string' ? response.message : '',
|
|
183
|
+
duration_ms: duration,
|
|
184
|
+
note: typeof response.message === 'string' ? response.message : code,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (response.ok !== true) {
|
|
188
|
+
return { status: 'invalid_output', provider_id: provider.id, duration_ms: duration, note: 'provider 响应缺 ok 字段' };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
status: 'active',
|
|
192
|
+
provider_id: provider.id,
|
|
193
|
+
data: response.data,
|
|
194
|
+
duration_ms: duration,
|
|
195
|
+
bytes_out: Buffer.byteLength(String(proc.stdout || ''), 'utf8'),
|
|
196
|
+
note: '',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function appendAudit(entry) {
|
|
201
|
+
try {
|
|
202
|
+
const home = providerHome();
|
|
203
|
+
fs.mkdirSync(home, { recursive: true });
|
|
204
|
+
fs.appendFileSync(
|
|
205
|
+
path.join(home, 'provider-audit.jsonl'),
|
|
206
|
+
JSON.stringify(Object.assign({ schema: SCHEMA, ts: new Date().toISOString() }, entry)) + '\n',
|
|
207
|
+
'utf8',
|
|
208
|
+
);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// 审计失败不影响调用
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runCapability(capability, payload) {
|
|
215
|
+
const config = readConfig();
|
|
216
|
+
if (config.status === 'not_installed') {
|
|
217
|
+
return { status: 'not_installed', provider_id: '', note: '未配置扩展提供方' };
|
|
218
|
+
}
|
|
219
|
+
if (config.status === 'error') {
|
|
220
|
+
return { status: 'error', provider_id: '', note: config.error };
|
|
221
|
+
}
|
|
222
|
+
const provider = pickProvider(config, capability);
|
|
223
|
+
if (!provider) {
|
|
224
|
+
return { status: 'not_installed', provider_id: '', note: '未配置声明 ' + capability + ' 的提供方' };
|
|
225
|
+
}
|
|
226
|
+
const result = invokeProvider(provider, capability, payload);
|
|
227
|
+
appendAudit({
|
|
228
|
+
capability,
|
|
229
|
+
provider_id: result.provider_id,
|
|
230
|
+
status: result.status,
|
|
231
|
+
duration_ms: result.duration_ms,
|
|
232
|
+
bytes_out: result.bytes_out || 0,
|
|
233
|
+
});
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
SCHEMA,
|
|
239
|
+
DEFAULT_TIMEOUT_MS,
|
|
240
|
+
MAX_TIMEOUT_MS,
|
|
241
|
+
MAX_STDOUT_BYTES,
|
|
242
|
+
KNOWN_CAPABILITIES,
|
|
243
|
+
providerHome,
|
|
244
|
+
configPath,
|
|
245
|
+
readConfig,
|
|
246
|
+
pickProvider,
|
|
247
|
+
runCapability,
|
|
248
|
+
};
|