@yottameta/yotta-skills 0.7.2 → 0.8.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/CHANGELOG.md +11 -1
- package/README.md +8 -8
- package/README.zh-CN.md +12 -12
- package/SKILL.md +16 -9
- package/bin/yotta-skills.js +126 -70
- package/lib/install-evidence.js +18 -0
- package/lib/install-pipeline.js +212 -0
- package/lib/manifest.js +131 -0
- package/lib/verify-gate.js +81 -0
- package/package.json +1 -1
- package/references/faq.md +6 -2
- package/references/install-flow.md +46 -7
- package/references/skill-list.md +1 -1
- package/references/tutorial.md +10 -6
- package/scripts/yotta-skills-mcp.py +1 -1
- package/skills.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
## v0.8.0 (2026-09-12)
|
|
2
|
+
|
|
3
|
+
**P0-2.1 安装编排与元信自举**:
|
|
4
|
+
|
|
5
|
+
- 安装链路读取包内 `skill-manifest.json`,缺失时使用 `skills.json` 家族默认契约。
|
|
6
|
+
- 元信缺失时自动自举,随后对所有家族包执行装前扫描。
|
|
7
|
+
- `DO NOT INSTALL` 和扫描失败阻断安装;`CAUTION` / `REVIEW` 继续但显示风险。
|
|
8
|
+
- 安装改为快照、暂存、原子切换,失败不删除旧版本。
|
|
9
|
+
- 新增 `~/.yottaskills/install-log.jsonl` 安装证据。
|
|
10
|
+
- `--skip-scan` 仅保留为人工应急路径,并记录 `explicit-unverified`。
|
|
11
|
+
|
|
1
12
|
## v0.7.2 (2026-09-10)
|
|
2
13
|
|
|
3
14
|
skills.json 清单同步(元序路径模型澄清升版后,元阁安装清单随包更新):
|
|
@@ -42,7 +53,6 @@ skills.json 清单同步(评测批 2 六技能升版后,元阁安装清单
|
|
|
42
53
|
## v0.5.1 (2026-09-06)
|
|
43
54
|
|
|
44
55
|
skills.json 三处滞后同步修复(yotta-skills 0.5.0→0.5.1):元真 0.1.2→0.1.3 / 元忆 0.8.5→0.10.1 / 元守 0.1.1→0.2.1 + updated 改期 2026-09-06 + yotta-skills 四件对齐 0.5.1 + 技能分发清单副本 + 插件同步。
|
|
45
|
-
# 更新日志
|
|
46
56
|
|
|
47
57
|
## v0.5.0 (2026-08-31)
|
|
48
58
|
|
package/README.md
CHANGED
|
@@ -29,12 +29,12 @@ Installing the whole YottaMeta family used to mean running <code>npx</code> for
|
|
|
29
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.
|
|
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
|
-
- **Update check / auto-update** — <code>update --check</code> compares installed skill versions against the npm registry and reports (no changes;
|
|
32
|
+
- **Update check / auto-update** — <code>update --check</code> compares installed skill versions against the npm registry and reports (no changes; run manually or from doctor / release checks); <code>update --auto</code> upgrades the installed YottaMeta family to the latest (non-family skills are never auto-updated).
|
|
33
33
|
- **Idempotent** — a skill already at the manifest version is skipped; re-running is safe.
|
|
34
|
-
- **Pre-install
|
|
34
|
+
- **Pre-install gate** — family installs read the package manifest, bootstrap yotta-verify (元信) when it is missing, then scan each package. <code>DO NOT INSTALL</code> blocks; caution/review continue with visible risk. Old versions are snapshotted before replacement.
|
|
35
35
|
- **Inventory / re-index** - scan the skill directories on this machine and keep a local registry (<code>~/.yottaskills/registry.json</code>); self-contained, no other skills required. Newly installed skills are discovered automatically: <code>install</code> / <code>update</code> re-index the registry afterwards, and <code>--reindex</code> re-scans on demand (e.g. at session start). An optional <code>yotta-skills</code> MCP (on-demand, not resident) exposes <code>list_installed_skills</code> / <code>describe_skill</code> / <code>reindex</code> / <code>route_request</code>; see <code>SKILL.md</code> for the config.
|
|
36
36
|
|
|
37
|
-
Boundaries: it only downloads, places and summarizes — it does **not** develop skill content, does **not** bundle any skill body, and does **not** use <code>-g</code> global installs.
|
|
37
|
+
Boundaries: it only downloads, places, gates and summarizes — it does **not** develop skill content, does **not** bundle any skill body, and does **not** use <code>-g</code> global installs. Besides the target directory, it keeps its registry, snapshots and install evidence under <code>~/.yottaskills</code>.
|
|
38
38
|
|
|
39
39
|
## Quick start
|
|
40
40
|
|
|
@@ -57,7 +57,7 @@ npx -y @yottameta/yotta-skills update --agent codex
|
|
|
57
57
|
# Check for updates (read-only, no changes); good to run at session start
|
|
58
58
|
npx -y @yottameta/yotta-skills update --check
|
|
59
59
|
|
|
60
|
-
# Check and auto-update the installed YottaMeta family (pre-install
|
|
60
|
+
# Check and auto-update the installed YottaMeta family (pre-install gate applies; non-family skills untouched)
|
|
61
61
|
npx -y @yottameta/yotta-skills update --auto
|
|
62
62
|
|
|
63
63
|
# Preview what would be installed (no network, no changes)
|
|
@@ -85,7 +85,7 @@ Requirements: Node.js 18+, npm, and system <code>tar</code> (built into Windows
|
|
|
85
85
|
| `install <skill>... [--agent <name> \| --dir <path>]` | Install only the given skills |
|
|
86
86
|
| `update [--agent <name> \| --dir <path>]` | Incremental update: add missing, upgrade version-skewed |
|
|
87
87
|
| `update --check` | Read-only update check: compare installed versions against the npm registry and report (exit 0 = up to date, 3 = updates available, 1 = could not check; run with `--registry <url>` to target a mirror); no changes |
|
|
88
|
-
| `update --auto` | Check and automatically update the installed YottaMeta family to the latest (runs the pre-install
|
|
88
|
+
| `update --auto` | Check and automatically update the installed YottaMeta family to the latest (runs the pre-install gate; never auto-updates non-family skills) |
|
|
89
89
|
| `--registry <url>` | npm registry to check against (default https://registry.npmjs.org/; `YOTTA_SKILLS_REGISTRY` overrides) |
|
|
90
90
|
| `--inventory` | Inventory installed skills: scan skill directories and update the local registry (self-contained); `--json` for JSON, `--project` adds project-level dirs |
|
|
91
91
|
| `--reindex` | Re-index the registry: re-scan skill directories and merge changes incrementally (session start / after installing skills; `--rescan` is a synonym); `--json` for JSON |
|
|
@@ -94,9 +94,9 @@ Requirements: Node.js 18+, npm, and system <code>tar</code> (built into Windows
|
|
|
94
94
|
| `--dry-run` | Preview the install / update list; no network, no changes |
|
|
95
95
|
| `--pin` | Lock the exact manifest versions (default: range, latest patch of the same major) |
|
|
96
96
|
| `--force` | Reinstall even when already at the latest version |
|
|
97
|
-
| `--skip-scan` |
|
|
97
|
+
| `--skip-scan` | Manual escape hatch: skip the verifier gate and mark the result `explicit-unverified`; never used by `update --auto` |
|
|
98
98
|
| `--npm <path>` | Specify the npm executable |
|
|
99
|
-
| `--python <path>` | Specify the python executable (used for
|
|
99
|
+
| `--python <path>` | Specify the python executable (used for yotta-verify scan) |
|
|
100
100
|
| `--verify <path>` | Specify the yotta_verify.py path |
|
|
101
101
|
| `-h, --help` / `-v, --version` | Help / version |
|
|
102
102
|
|
|
@@ -161,7 +161,7 @@ The full 22-skill manifest with Chinese names, package names, versions and descr
|
|
|
161
161
|
|
|
162
162
|
## How it works
|
|
163
163
|
|
|
164
|
-
For each skill in the manifest: `npm pack <pkg>@<spec>` into a temp directory → `tar -xzf` →
|
|
164
|
+
For each skill in the manifest: `npm pack <pkg>@<spec>` into a temp directory → `tar -xzf` → read the package manifest → run the yotta-verify pre-install gate (bootstrap yotta-verify first when missing) → snapshot the old version and stage the new one → atomically replace `<dest>/<slug>` → summary report. After `install` / `update` finishes, the local skill registry is re-indexed automatically so newly installed skills show up in `--inventory` / `--reindex` (disable with `--no-reindex`). Details: `references/install-flow.md`; beginner tutorial (Chinese): `references/tutorial.md`.
|
|
165
165
|
|
|
166
166
|
## Development & validation
|
|
167
167
|
|
package/README.zh-CN.md
CHANGED
|
@@ -32,13 +32,13 @@
|
|
|
32
32
|
- **编排路由**--`--route` 按需求摘要输出候选组合、调用顺序、技能角色、置信度、依据、已装/缺失状态与安装命令;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);只建议安装,不自动安装。
|
|
33
33
|
- **安装**——装全家(或指定技能)到智能体默认用户级目录或任意目录。
|
|
34
34
|
- **更新**——增量更新:补齐缺失技能、升级版本不一致的技能。
|
|
35
|
-
- **更新检查 / 自动更新**——<code>update --check</code> 只读比对已装技能版本与 npm
|
|
35
|
+
- **更新检查 / 自动更新**——<code>update --check</code> 只读比对已装技能版本与 npm 注册表并报告(不改动,手动或由 doctor / 发布前检查触发);<code>update --auto</code> 检查后自动把已装元阁家族升到最新(非元阁家族技能绝不自动更新)。
|
|
36
36
|
- **幂等**——已在清单版本的技能跳过;重复运行安全。
|
|
37
|
-
-
|
|
37
|
+
- **装前门禁**——家族安装先读取包内 manifest,元信(yotta-verify)缺失时自动自举,再逐个扫描。`DO NOT INSTALL` 阻断;`CAUTION` / `REVIEW` 继续但显示风险。旧版本会先做快照再替换。
|
|
38
38
|
- **盘点 / re-index**--扫描本机各智能体技能目录,维护本地注册表(<code>~/.yottaskills/registry.json</code>);自包含,不需要任何其他技能。新装技能自动被发现:<code>install</code> / <code>update</code> 完成后自动重扫注册表,<code>--reindex</code> 可随时手动重扫(如会话开工)。可选 <code>yotta-skills</code> MCP(按需加载、不常驻)提供 <code>list_installed_skills</code> / <code>describe_skill</code> / <code>reindex</code> / <code>route_request</code> 四工具,配置见 <code>SKILL.md</code>。
|
|
39
39
|
|
|
40
|
-
边界:只做「下载 + 落位 + 汇总」——**不**开发技能内容、**不**内置任何技能本体、**不**用 <code>-g</code>
|
|
41
|
-
|
|
40
|
+
边界:只做「下载 + 落位 + 门禁 + 汇总」——**不**开发技能内容、**不**内置任何技能本体、**不**用 <code>-g</code>
|
|
41
|
+
全局安装;除目标目录外,会在 <code>~/.yottaskills</code> 下保留注册表、快照与安装证据。
|
|
42
42
|
|
|
43
43
|
## 快速使用
|
|
44
44
|
|
|
@@ -58,10 +58,10 @@ npx -y @yottameta/yotta-skills install yotta-memory yotta-verify --dir ~/my-skil
|
|
|
58
58
|
# 增量更新已装技能
|
|
59
59
|
npx -y @yottameta/yotta-skills update --agent codex
|
|
60
60
|
|
|
61
|
-
#
|
|
61
|
+
# 只读检查更新(不改动;手动或由 doctor / 发布前检查触发)
|
|
62
62
|
npx -y @yottameta/yotta-skills update --check
|
|
63
63
|
|
|
64
|
-
#
|
|
64
|
+
# 检查后自动更新已装元阁家族(含装前门禁;非元阁家族技能不自动更新)
|
|
65
65
|
npx -y @yottameta/yotta-skills update --auto
|
|
66
66
|
|
|
67
67
|
# 预览将安装清单(不联网、不改动)
|
|
@@ -89,7 +89,7 @@ npx -y @yottameta/yotta-skills --reindex
|
|
|
89
89
|
| `install <skill>... [--agent <name> \| --dir <path>]` | 只装指定的一个或多个技能 |
|
|
90
90
|
| `update [--agent <name> \| --dir <path>]` | 增量更新:补齐缺失、升级版本不一致的技能 |
|
|
91
91
|
| `update --check` | 只读更新检查:比对已装版本与 npm 注册表并报告(退出码 0=最新 / 3=有更新 / 1=无法检查;可用 `--registry <url>` 指定镜像);不改动 |
|
|
92
|
-
| `update --auto` |
|
|
92
|
+
| `update --auto` | 检查后自动更新已装元阁家族到最新(含装前门禁;非元阁家族技能绝不自动更新) |
|
|
93
93
|
| `--registry <url>` | 检查的 npm registry 地址(默认 https://registry.npmjs.org/;`YOTTA_SKILLS_REGISTRY` 覆盖) |
|
|
94
94
|
| `--inventory` | 盘点已装技能:扫描技能目录并更新本地注册表(自包含);`--json` 输出 JSON、`--project` 附扫项目级目录 |
|
|
95
95
|
| `--reindex` | 重扫注册表:扫描技能目录并增量合并变化(会话开工 / 装技能后自动调用;`--rescan` 同义);`--json` 输出 JSON |
|
|
@@ -98,9 +98,9 @@ npx -y @yottameta/yotta-skills --reindex
|
|
|
98
98
|
| `--dry-run` | 预览将执行的安装 / 更新清单;不联网、不改动 |
|
|
99
99
|
| `--pin` | 锁死清单精确版本(默认 range:跟随同 major 最新 patch) |
|
|
100
100
|
| `--force` | 已是最新也重新安装 |
|
|
101
|
-
| `--skip-scan` |
|
|
101
|
+
| `--skip-scan` | 人工应急路径:跳过元信门禁并标记 `explicit-unverified`;`update --auto` 不使用该开关 |
|
|
102
102
|
| `--npm <path>` | 指定 npm 可执行文件 |
|
|
103
|
-
| `--python <path>` | 指定 python
|
|
103
|
+
| `--python <path>` | 指定 python 可执行文件(元信 scan 用) |
|
|
104
104
|
| `--verify <path>` | 指定 yotta_verify.py 路径 |
|
|
105
105
|
| `-h, --help` / `-v, --version` | 帮助 / 版本 |
|
|
106
106
|
|
|
@@ -169,8 +169,8 @@ bash install.sh --list # 列出智能体 -> 默认目录
|
|
|
169
169
|
|
|
170
170
|
## 工作原理
|
|
171
171
|
|
|
172
|
-
对清单里每个技能:`npm pack <pkg>@<spec>` 到临时目录 → `tar -xzf` 解压 →
|
|
173
|
-
|
|
172
|
+
对清单里每个技能:`npm pack <pkg>@<spec>` 到临时目录 → `tar -xzf` 解压 → 读取 manifest →
|
|
173
|
+
元信装前门禁 → 快照旧版本并暂存新版本 → 原子替换 `<dest>/<slug>` → 汇总报告。`install` / `update` 完成后自动重扫本地技能注册表,
|
|
174
174
|
新装技能随即出现在 `--inventory` / `--reindex` 里(可用 `--no-reindex` 关闭)。
|
|
175
175
|
细节见 `references/install-flow.md`;新手中文教程见 `references/tutorial.md`。
|
|
176
176
|
|
|
@@ -181,7 +181,7 @@ bash install.sh --list # 列出智能体 -> 默认目录
|
|
|
181
181
|
npm test
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
测试覆盖 `--list`、临时目录安装断言、幂等、`--pin`、`update
|
|
184
|
+
测试覆盖 `--list`、临时目录安装断言、幂等、`--pin`、`update`、异常路径、manifest 校验、元信自举与装前门禁
|
|
185
185
|
(用 fake npm 不联网)。
|
|
186
186
|
|
|
187
187
|
## 参考文档
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-skills
|
|
3
|
-
version: 0.
|
|
4
|
-
description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么自动装+自动用」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录;盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工可先跑 --reindex 与 update --check(联网只读检查更新);自包含零依赖,不依赖任何元技能);MCP 按需加载(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --auto(家族自动更新)/ --inventory / --reindex / --dry-run 预览 / --pin 锁版本。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index」,不含技能本体、不做技能内容开发、不 -g
|
|
3
|
+
version: 0.8.0
|
|
4
|
+
description: 元阁 -- 元阁全家技能的总编排策划 + 编排路由 + 一键安装器 + 技能盘点。路由层:--route / route_request 按需求摘要给出候选组合、调用顺序、角色、置信度、依据、已装/缺失状态与安装命令,只建议不自动安装;非元阁家族已装技能按 frontmatter description 机械匹配作并列候选(标注来源与未扫描状态,只读不自动调用);策划层:按场景给出「该组合哪几个元技能、组合强在哪、怎么自动装+自动用」;安装层:一条命令把 YottaMeta 已发布的全部 yotta-* 技能装进指定智能体或目录;盘点层:--inventory / --reindex 扫描本机已装技能生成/更新注册表,新装技能自动被发现(install/update 后自动 re-index,会话开工可先跑 --reindex 与 update --check(联网只读检查更新);自包含零依赖,不依赖任何元技能);MCP 按需加载(可选:list_installed_skills/describe_skill/reindex/route_request,不常驻,未加载降级 CLI)。支持 --list 清单 / --route 路由 / install / update / update --check(只读检查)/ update --auto(家族自动更新)/ --inventory / --reindex / --dry-run 预览 / --pin 锁版本。触发:需要批量安装或更新元阁全家技能、按场景组合多个元技能、路由或判断该用哪些技能、盘点或查看本机已装技能、重扫技能注册表、给某个智能体或目录一次性铺齐 yotta-* 技能、预览安装清单、锁版本安装、或用户说 元阁/装全家/一次装齐/yotta-skills/install-all/更新全家/检查更新/自动更新/该用哪个技能/路由技能/盘点技能/查看已装技能 等。边界(Do NOT trigger):只做「组合策划 + 静态路由建议 + 清单 + 下载 + 落位 + 汇总 + 盘点 + re-index」,不含技能本体、不做技能内容开发、不 -g 污染全局、不自动安装缺失技能;家族安装先自举或调用元信装前门禁,DO NOT INSTALL 阻断,非元阁家族包不自动安装。
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
7
|
zh_name: 元阁
|
|
@@ -132,9 +132,9 @@ npx -y @yottameta/yotta-skills --reindex
|
|
|
132
132
|
| `--dry-run` | 预览将执行的安装 / 更新清单;不联网、不改动 |
|
|
133
133
|
| `--pin` | 锁死清单精确版本(默认 range:跟随同 major 最新 patch) |
|
|
134
134
|
| `--force` | 已是最新也重新安装 |
|
|
135
|
-
| `--skip-scan` |
|
|
135
|
+
| `--skip-scan` | 人工应急路径:跳过元信门禁并标记 `explicit-unverified`;不得用于 `update --auto` |
|
|
136
136
|
| `--npm <path>` | 指定 npm 可执行文件 |
|
|
137
|
-
| `--python <path>` | 指定 python
|
|
137
|
+
| `--python <path>` | 指定 python 可执行文件(元信 scan 用) |
|
|
138
138
|
| `--verify <path>` | 指定 yotta_verify.py 路径 |
|
|
139
139
|
| `-h, --help` / `-v, --version` | 帮助 / 版本 |
|
|
140
140
|
|
|
@@ -235,12 +235,19 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
|
|
|
235
235
|
- 是否「已是最新」由目标目录 `<slug>/SKILL.md` 的 frontmatter `version` 与清单比对,
|
|
236
236
|
一致即跳过(幂等)。
|
|
237
237
|
|
|
238
|
-
##
|
|
238
|
+
## 元信装前门禁
|
|
239
|
+
|
|
240
|
+
家族安装默认执行装前扫描。若本机没有元信(yotta-verify),元阁先按 `skills.json`
|
|
241
|
+
安装元信自身,再扫描待装技能。安装前会先读取包内 manifest、校验身份,再进入元信门禁:
|
|
242
|
+
|
|
243
|
+
- `SAFE TO INSTALL`:继续安装;
|
|
244
|
+
- `INSTALL WITH CAUTION` / `REVIEW REQUIRED`:继续安装,但显示风险并写入证据;
|
|
245
|
+
- `DO NOT INSTALL` 或扫描失败:阻断,不替换旧版本。
|
|
239
246
|
|
|
240
|
-
安装时若检测到元信(yotta-verify)引擎,会对每个待装技能先跑一次装前摘要并打印 verdict
|
|
241
|
-
与计数——仅提示、不拦截;verdict 为 DO NOT INSTALL 时会额外提示人工复核。
|
|
242
247
|
引擎查找顺序:`--verify` 指定路径 → 环境变量 `YOTTA_SKILLS_VERIFY` → 目标目录下已装的
|
|
243
|
-
`yotta-verify/scripts/yotta_verify.py
|
|
248
|
+
`yotta-verify/scripts/yotta_verify.py`。`--skip-scan` 只保留为人工应急路径,使用时输出
|
|
249
|
+
`explicit-unverified` 并写入 `~/.yottaskills/install-log.jsonl`;`update --auto` 不会使用
|
|
250
|
+
该开关。
|
|
244
251
|
|
|
245
252
|
## 支持智能体
|
|
246
253
|
|
|
@@ -254,7 +261,7 @@ npx -y @yottameta/yotta-skills --route "检查代码质量,别糊弄" --json
|
|
|
254
261
|
|---|---|
|
|
255
262
|
| `YOTTA_SKILLS_NPM` | 指定 npm 可执行文件(同 `--npm`) |
|
|
256
263
|
| `YOTTA_SKILLS_NPM_FLAGS` | 追加传给 `npm pack` 的参数(按空白拆分,如 `--registry=...`) |
|
|
257
|
-
| `YOTTA_SKILLS_PYTHON` | 指定 python
|
|
264
|
+
| `YOTTA_SKILLS_PYTHON` | 指定 python 可执行文件(元信 scan 用,同 `--python`) |
|
|
258
265
|
| `YOTTA_SKILLS_VERIFY` | 指定 yotta_verify.py 路径(同 `--verify`) |
|
|
259
266
|
| `YOTTA_SKILLS_MANIFEST` | 指定技能清单 JSON 路径(默认随包 skills.json) |
|
|
260
267
|
|
package/bin/yotta-skills.js
CHANGED
|
@@ -24,6 +24,9 @@ const os = require('os');
|
|
|
24
24
|
const { spawnSync } = require('child_process');
|
|
25
25
|
const http = require('http');
|
|
26
26
|
const https = require('https');
|
|
27
|
+
const evidenceLib = require('../lib/install-evidence');
|
|
28
|
+
const gateLib = require('../lib/verify-gate');
|
|
29
|
+
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
27
30
|
|
|
28
31
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
29
32
|
let VERSION = '0.2.1';
|
|
@@ -343,19 +346,27 @@ async function runUpdateAuto(opts, dest) {
|
|
|
343
346
|
}
|
|
344
347
|
out('');
|
|
345
348
|
out('检测到 ' + r.updates + ' 个家族技能可更新,开始自动更新(安装管线,含装前安全扫描):');
|
|
346
|
-
var ok = 0, failed2 = 0;
|
|
349
|
+
var ok = 0, failed2 = 0, autoExitCode = 0;
|
|
347
350
|
for (var i = 0; i < r.rows.length; i++) {
|
|
348
351
|
var row = r.rows[i];
|
|
349
352
|
if (!row.hasUpdate) continue;
|
|
350
|
-
var res = installOne(
|
|
353
|
+
var res = installOne(
|
|
354
|
+
Object.assign({}, row.family, { version: row.latest }),
|
|
355
|
+
dest,
|
|
356
|
+
Object.assign({}, opts, { force: true, pin: true, skipScan: false }),
|
|
357
|
+
);
|
|
351
358
|
if (res.status === 'ok') ok++;
|
|
352
359
|
else if (res.status === 'skip') ok++;
|
|
353
|
-
else
|
|
360
|
+
else {
|
|
361
|
+
failed2++;
|
|
362
|
+
autoExitCode = Math.max(autoExitCode, res.exitCode || 1);
|
|
363
|
+
out(' ✘ ' + row.slug + ' 失败: ' + res.note);
|
|
364
|
+
}
|
|
354
365
|
}
|
|
355
366
|
out('');
|
|
356
367
|
out('自动更新汇总: 成功 ' + ok + ' / 失败 ' + failed2);
|
|
357
|
-
maybeAutoReindex(opts, dest);
|
|
358
|
-
return { code: failed2 > 0 ?
|
|
368
|
+
if (failed2 === 0) maybeAutoReindex(opts, dest);
|
|
369
|
+
return { code: failed2 > 0 ? autoExitCode : 0 };
|
|
359
370
|
}
|
|
360
371
|
|
|
361
372
|
function shouldSkip(name, isFile) {
|
|
@@ -399,17 +410,12 @@ function detectProjectDir() {
|
|
|
399
410
|
return null;
|
|
400
411
|
}
|
|
401
412
|
|
|
402
|
-
// ── 元信 scan
|
|
413
|
+
// ── 元信 scan 与安装门禁 ──────────────────────────────────────────────────
|
|
403
414
|
function findVerifyEngine(dest, opts) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
if (process.env.YOTTA_SKILLS_VERIFY) cands.push(path.resolve(process.env.YOTTA_SKILLS_VERIFY));
|
|
407
|
-
if (dest) cands.push(path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py'));
|
|
408
|
-
for (const c of cands) {
|
|
409
|
-
try { if (fs.statSync(c).isFile()) return c; } catch (_) { /* next */ }
|
|
410
|
-
}
|
|
411
|
-
return null;
|
|
415
|
+
const registry = require('../lib/skills-scan').readRegistry();
|
|
416
|
+
return gateLib.findVerifier({ dest, opts, registry });
|
|
412
417
|
}
|
|
418
|
+
|
|
413
419
|
function findPython(opts) {
|
|
414
420
|
const cands = [];
|
|
415
421
|
if (opts.python) cands.push(opts.python);
|
|
@@ -424,19 +430,29 @@ function findPython(opts) {
|
|
|
424
430
|
}
|
|
425
431
|
return null;
|
|
426
432
|
}
|
|
433
|
+
|
|
427
434
|
function runScan(engine, skillDir) {
|
|
428
435
|
const python = findPython({});
|
|
429
|
-
if (!python) return { ok: false,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
436
|
+
if (!python) return { ok: false, error: '未找到 Python(元信 scan 需要 Python 3.8+)' };
|
|
437
|
+
return gateLib.runVerifier(engine, skillDir, { python, spawnSync });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function scanTarget(engine, skillDir) {
|
|
441
|
+
const scan = runScan(engine, skillDir);
|
|
442
|
+
if (scan.ok) {
|
|
443
|
+
const counts = scan.counts || {};
|
|
444
|
+
const line = 'critical ' + (counts.critical || 0) + ' / high ' + (counts.high || 0) +
|
|
445
|
+
' / medium ' + (counts.medium || 0) + ' / low ' + (counts.low || 0) +
|
|
446
|
+
' / info ' + (counts.info || 0);
|
|
447
|
+
out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
|
|
448
|
+
if (scan.verdict === gateLib.BLOCK) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,已阻断安装。');
|
|
449
|
+
else if (scan.verdict === gateLib.CAUTION || scan.verdict === gateLib.REVIEW) {
|
|
450
|
+
out(' ⚠ 元信 verdict 为 ' + scan.verdict + ',继续安装并保留风险证据。');
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
out(' 元信 scan: ' + scan.error);
|
|
439
454
|
}
|
|
455
|
+
return scan;
|
|
440
456
|
}
|
|
441
457
|
|
|
442
458
|
// ── 安装 ───────────────────────────────────────────────────────────────────
|
|
@@ -468,6 +484,13 @@ function runNpmPack(skill, opts, packDir) {
|
|
|
468
484
|
}
|
|
469
485
|
|
|
470
486
|
function extractTarball(tarball, extractDir) {
|
|
487
|
+
const listed = spawnSync(tarBin(), ['-tzf', tarball], { encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
|
|
488
|
+
if (listed.status !== 0) {
|
|
489
|
+
return { error: (listed.stderr || listed.stdout || 'tar 列表读取失败').trim().split('\n').pop() };
|
|
490
|
+
}
|
|
491
|
+
const entries = String(listed.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
492
|
+
const unsafe = entries.find((entry) => !isSafeTarEntry(entry));
|
|
493
|
+
if (unsafe) return { error: '压缩包包含不安全路径: ' + unsafe };
|
|
471
494
|
const r = spawnSync(tarBin(), ['-xzf', tarball, '-C', extractDir], { encoding: 'utf8', timeout: 120000 });
|
|
472
495
|
if (r.status !== 0) return { error: (r.stderr || r.stdout || 'tar 解压失败').trim().split('\n').pop() };
|
|
473
496
|
const pkgDir = path.join(extractDir, 'package');
|
|
@@ -477,72 +500,94 @@ function extractTarball(tarball, extractDir) {
|
|
|
477
500
|
|
|
478
501
|
const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '__pycache__']);
|
|
479
502
|
|
|
480
|
-
function
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
if (
|
|
484
|
-
return {
|
|
503
|
+
function ensureGate(context) {
|
|
504
|
+
const { skill, extracted, dest, opts } = context;
|
|
505
|
+
const current = findVerifyEngine(dest, opts);
|
|
506
|
+
if (current) {
|
|
507
|
+
return {
|
|
508
|
+
ok: true,
|
|
509
|
+
engine: current,
|
|
510
|
+
mode: opts.bootstrap ? 'trusted-bootstrap' : 'installed',
|
|
511
|
+
};
|
|
485
512
|
}
|
|
513
|
+
if (skill.slug === 'yotta-verify') {
|
|
514
|
+
const engine = path.join(extracted.pkgDir, 'scripts', 'yotta_verify.py');
|
|
515
|
+
if (!fs.existsSync(engine)) return { ok: false, error: '元信包内缺少 scripts/yotta_verify.py' };
|
|
516
|
+
return { ok: true, engine, mode: 'trusted-bootstrap' };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const verifier = findSkill('yotta-verify');
|
|
520
|
+
if (!verifier) return { ok: false, error: 'skills.json 缺少 yotta-verify' };
|
|
486
521
|
let tmp;
|
|
487
522
|
try {
|
|
488
|
-
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-
|
|
523
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-verify-bootstrap-'));
|
|
489
524
|
const packDir = path.join(tmp, 'pack');
|
|
490
525
|
fs.mkdirSync(packDir, { recursive: true });
|
|
491
|
-
const packed = runNpmPack(
|
|
492
|
-
if (packed.error) {
|
|
493
|
-
if (packed.detail) out(' ' + packed.detail.split(/\r?\n/).filter(Boolean).slice(-12).join('\n '));
|
|
494
|
-
return { skill, status: 'fail', version: null, note: packed.error };
|
|
495
|
-
}
|
|
526
|
+
const packed = runNpmPack(verifier, opts, packDir);
|
|
527
|
+
if (packed.error) return { ok: false, error: '元信自举下载失败: ' + packed.error };
|
|
496
528
|
const extractDir = path.join(tmp, 'extract');
|
|
497
529
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
518
|
-
fs.mkdirSync(target, { recursive: true });
|
|
519
|
-
copyDir(extracted.pkgDir, target, COPY_SKIP);
|
|
520
|
-
return { skill, status: 'ok', version: packed.resolved || skill.version, note: null };
|
|
521
|
-
} catch (e) {
|
|
522
|
-
return { skill, status: 'fail', version: null, note: e.message };
|
|
530
|
+
const extractedVerifier = extractTarball(packed.tarball, extractDir);
|
|
531
|
+
if (extractedVerifier.error) return { ok: false, error: '元信自举解包失败: ' + extractedVerifier.error };
|
|
532
|
+
const engine = path.join(extractedVerifier.pkgDir, 'scripts', 'yotta_verify.py');
|
|
533
|
+
if (!fs.existsSync(engine)) return { ok: false, error: '元信包内缺少 scripts/yotta_verify.py' };
|
|
534
|
+
|
|
535
|
+
out(' 元信未安装,使用可信源包自举并先自扫。');
|
|
536
|
+
const result = installOne(verifier, dest, {
|
|
537
|
+
...opts,
|
|
538
|
+
force: true,
|
|
539
|
+
skipScan: false,
|
|
540
|
+
bootstrap: true,
|
|
541
|
+
verify: engine,
|
|
542
|
+
});
|
|
543
|
+
if (result.status !== 'ok') return { ok: false, error: '元信自举失败: ' + result.note };
|
|
544
|
+
const installedEngine = path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py');
|
|
545
|
+
if (!fs.existsSync(installedEngine)) return { ok: false, error: '元信自举后未找到安装引擎' };
|
|
546
|
+
return { ok: true, engine: installedEngine, mode: 'trusted-bootstrap' };
|
|
523
547
|
} finally {
|
|
524
548
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
525
549
|
}
|
|
526
550
|
}
|
|
527
551
|
|
|
552
|
+
const installOne = createInstaller({
|
|
553
|
+
runNpmPack,
|
|
554
|
+
extractTarball,
|
|
555
|
+
copyDir: (src, dst) => copyDir(src, dst, COPY_SKIP),
|
|
556
|
+
readInstalledVersion,
|
|
557
|
+
ensureGate,
|
|
558
|
+
scanTarget,
|
|
559
|
+
appendEvidence: evidenceLib.appendEvidence,
|
|
560
|
+
});
|
|
561
|
+
|
|
528
562
|
function runInstall(opts, dest) {
|
|
529
563
|
const skills = selectSkills(opts);
|
|
530
564
|
out('yotta-skills(元阁)v' + VERSION + ' —— 安装 ' + skills.length + ' 个技能 -> ' + dest);
|
|
531
565
|
out('版本策略: ' + (opts.pin ? 'pin(锁死清单精确版本)' : 'range(' + skillRange(skills[0]) + ',跟随最新 patch;--pin 锁死)'));
|
|
532
566
|
const results = [];
|
|
533
567
|
let failed = 0;
|
|
568
|
+
let exitCode = 0;
|
|
534
569
|
for (const s of skills) {
|
|
535
570
|
const r = installOne(s, dest, opts);
|
|
536
571
|
results.push(r);
|
|
537
|
-
if (r.status === 'ok')
|
|
572
|
+
if (r.status === 'ok') {
|
|
573
|
+
if (r.gate && r.gate.mode === 'explicit-unverified') {
|
|
574
|
+
out(' ⚠ ' + s.slug.padEnd(22) + '未执行装前扫描(explicit-unverified)');
|
|
575
|
+
}
|
|
576
|
+
out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
|
|
577
|
+
}
|
|
538
578
|
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ',v' + r.version + ')');
|
|
539
|
-
else {
|
|
579
|
+
else {
|
|
580
|
+
failed++;
|
|
581
|
+
exitCode = Math.max(exitCode, r.exitCode || 1);
|
|
582
|
+
out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note);
|
|
583
|
+
}
|
|
540
584
|
}
|
|
541
585
|
const ok = results.filter(r => r.status === 'ok').length;
|
|
542
586
|
const skip = results.filter(r => r.status === 'skip').length;
|
|
543
587
|
out('');
|
|
544
588
|
out('汇总: 成功 ' + ok + ' / 跳过(已是最新) ' + skip + ' / 失败 ' + failed + '(共 ' + results.length + ')');
|
|
545
|
-
if (failed > 0) process.exitCode = 1;
|
|
589
|
+
if (failed > 0) process.exitCode = exitCode || 1;
|
|
590
|
+
return { failed, exitCode };
|
|
546
591
|
}
|
|
547
592
|
|
|
548
593
|
function runUpdate(opts, dest) {
|
|
@@ -550,6 +595,7 @@ function runUpdate(opts, dest) {
|
|
|
550
595
|
out('yotta-skills(元阁)v' + VERSION + ' —— 增量更新 -> ' + dest);
|
|
551
596
|
const results = [];
|
|
552
597
|
let failed = 0;
|
|
598
|
+
let exitCode = 0;
|
|
553
599
|
for (const s of skills) {
|
|
554
600
|
const existing = readInstalledVersion(path.join(dest, s.slug));
|
|
555
601
|
if (existing === s.version) {
|
|
@@ -559,15 +605,25 @@ function runUpdate(opts, dest) {
|
|
|
559
605
|
}
|
|
560
606
|
const r = installOne(s, dest, opts);
|
|
561
607
|
results.push(r);
|
|
562
|
-
if (r.status === 'ok')
|
|
608
|
+
if (r.status === 'ok') {
|
|
609
|
+
if (r.gate && r.gate.mode === 'explicit-unverified') {
|
|
610
|
+
out(' ⚠ ' + s.slug.padEnd(22) + '未执行装前扫描(explicit-unverified)');
|
|
611
|
+
}
|
|
612
|
+
out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
|
|
613
|
+
}
|
|
563
614
|
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ')');
|
|
564
|
-
else {
|
|
615
|
+
else {
|
|
616
|
+
failed++;
|
|
617
|
+
exitCode = Math.max(exitCode, r.exitCode || 1);
|
|
618
|
+
out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note);
|
|
619
|
+
}
|
|
565
620
|
}
|
|
566
621
|
const ok = results.filter(r => r.status === 'ok').length;
|
|
567
622
|
const skip = results.filter(r => r.status === 'skip').length;
|
|
568
623
|
out('');
|
|
569
624
|
out('汇总: 更新 ' + ok + ' / 已是最新 ' + skip + ' / 失败 ' + failed);
|
|
570
|
-
if (failed > 0) process.exitCode = 1;
|
|
625
|
+
if (failed > 0) process.exitCode = exitCode || 1;
|
|
626
|
+
return { failed, exitCode };
|
|
571
627
|
}
|
|
572
628
|
|
|
573
629
|
// ── 展示 ───────────────────────────────────────────────────────────────────
|
|
@@ -777,12 +833,12 @@ function main() {
|
|
|
777
833
|
process.exitCode = r.code;
|
|
778
834
|
});
|
|
779
835
|
} else {
|
|
780
|
-
runUpdate(opts, dest);
|
|
781
|
-
maybeAutoReindex(opts, dest);
|
|
836
|
+
const result = runUpdate(opts, dest);
|
|
837
|
+
if (!result.failed) maybeAutoReindex(opts, dest);
|
|
782
838
|
}
|
|
783
839
|
} else {
|
|
784
|
-
runInstall(opts, dest);
|
|
785
|
-
maybeAutoReindex(opts, dest);
|
|
840
|
+
const result = runInstall(opts, dest);
|
|
841
|
+
if (!result.failed) maybeAutoReindex(opts, dest);
|
|
786
842
|
}
|
|
787
843
|
}
|
|
788
844
|
|
|
@@ -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,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const manifestLib = require('./manifest');
|
|
6
|
+
const gateLib = require('./verify-gate');
|
|
7
|
+
|
|
8
|
+
function nowTag() {
|
|
9
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sleepSync(ms) {
|
|
13
|
+
const array = new Int32Array(new SharedArrayBuffer(4));
|
|
14
|
+
Atomics.wait(array, 0, 0, ms);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function renameWithRetry(from, to, options) {
|
|
18
|
+
const rename = (options && options.rename) || fs.renameSync;
|
|
19
|
+
const sleep = (options && options.sleep) || sleepSync;
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
return rename(from, to);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
lastError = error;
|
|
26
|
+
if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code) || attempt === 4) throw error;
|
|
27
|
+
sleep(100 * (attempt + 1));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw lastError;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isSafeTarEntry(entry) {
|
|
34
|
+
const value = String(entry || '').replace(/\\/g, '/');
|
|
35
|
+
if (value !== 'package' && !value.startsWith('package/')) return false;
|
|
36
|
+
if (value.startsWith('/') || /^[A-Za-z]:\//.test(value)) return false;
|
|
37
|
+
return !value.split('/').includes('..');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function copyTree(src, dst, copyDir) {
|
|
41
|
+
fs.rmSync(dst, { recursive: true, force: true });
|
|
42
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
43
|
+
copyDir(src, dst);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function restoreBackup(target, backup) {
|
|
47
|
+
if (!backup || !fs.existsSync(backup)) return;
|
|
48
|
+
if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
|
|
49
|
+
renameWithRetry(backup, target);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createInstaller(deps) {
|
|
53
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
54
|
+
|
|
55
|
+
function record(entry) {
|
|
56
|
+
return deps.appendEvidence(entry, { homeDir });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeRecord(entry) {
|
|
60
|
+
try {
|
|
61
|
+
return record(entry);
|
|
62
|
+
} catch (_) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return function installOne(skill, dest, opts) {
|
|
68
|
+
const target = path.join(dest, skill.slug);
|
|
69
|
+
const existingVersion = deps.readInstalledVersion(target);
|
|
70
|
+
if (!opts.force && existingVersion === skill.version) {
|
|
71
|
+
return { skill, status: 'skip', version: existingVersion, note: '已是最新', exitCode: 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let tmp = null;
|
|
75
|
+
let staged = null;
|
|
76
|
+
let snapshot = null;
|
|
77
|
+
let backup = null;
|
|
78
|
+
try {
|
|
79
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-skills-'));
|
|
80
|
+
const packDir = path.join(tmp, 'pack');
|
|
81
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
82
|
+
const packed = deps.runNpmPack(skill, opts, packDir);
|
|
83
|
+
if (packed.error) {
|
|
84
|
+
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'fail', error: packed.error });
|
|
85
|
+
return { skill, status: 'fail', version: null, note: packed.error, exitCode: 1 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const extractDir = path.join(tmp, 'extract');
|
|
89
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
90
|
+
const extracted = deps.extractTarball(packed.tarball, extractDir);
|
|
91
|
+
if (extracted.error) {
|
|
92
|
+
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'fail', error: extracted.error });
|
|
93
|
+
return { skill, status: 'fail', version: packed.resolved, note: extracted.error, exitCode: 1 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const resolvedSkill = { ...skill, version: packed.resolved || skill.version };
|
|
97
|
+
const loaded = manifestLib.loadManifest({ pkgDir: extracted.pkgDir, skill: resolvedSkill });
|
|
98
|
+
if (loaded.errors.length > 0) {
|
|
99
|
+
const note = loaded.errors.join('; ');
|
|
100
|
+
safeRecord({ event: 'manifest', skill: skill.slug, decision: 'block', errors: loaded.errors });
|
|
101
|
+
return { skill, status: 'fail', version: packed.resolved, note, exitCode: 6 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let gate;
|
|
105
|
+
let scan;
|
|
106
|
+
if (opts.skipScan) {
|
|
107
|
+
gate = { ok: true, engine: null, mode: 'explicit-unverified' };
|
|
108
|
+
scan = { decision: 'unverified', verdict: null, counts: null, warn: true, mode: 'explicit-unverified' };
|
|
109
|
+
} else {
|
|
110
|
+
gate = deps.ensureGate({ skill, extracted, dest, opts });
|
|
111
|
+
if (!gate.ok) {
|
|
112
|
+
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'block', error: gate.error });
|
|
113
|
+
return { skill, status: 'fail', version: packed.resolved, note: gate.error, exitCode: 5 };
|
|
114
|
+
}
|
|
115
|
+
const scanResult = deps.scanTarget(gate.engine, extracted.pkgDir);
|
|
116
|
+
if (!scanResult.ok) {
|
|
117
|
+
safeRecord({ event: 'before_install', skill: skill.slug, decision: 'block', error: scanResult.error });
|
|
118
|
+
return { skill, status: 'fail', version: packed.resolved, note: scanResult.error, exitCode: 5 };
|
|
119
|
+
}
|
|
120
|
+
const verdict = gateLib.evaluateVerdict(scanResult.verdict);
|
|
121
|
+
if (verdict.block) {
|
|
122
|
+
safeRecord({
|
|
123
|
+
event: 'before_install',
|
|
124
|
+
skill: skill.slug,
|
|
125
|
+
decision: 'block',
|
|
126
|
+
verdict: scanResult.verdict,
|
|
127
|
+
counts: scanResult.counts,
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
skill,
|
|
131
|
+
status: 'fail',
|
|
132
|
+
version: packed.resolved,
|
|
133
|
+
note: '元信 verdict: ' + scanResult.verdict,
|
|
134
|
+
exitCode: 5,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
scan = {
|
|
138
|
+
decision: verdict.decision,
|
|
139
|
+
verdict: scanResult.verdict,
|
|
140
|
+
counts: scanResult.counts,
|
|
141
|
+
warn: verdict.warn,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
146
|
+
const stagingRoot = path.join(dest, '.yottaskills-staging');
|
|
147
|
+
fs.mkdirSync(stagingRoot, { recursive: true });
|
|
148
|
+
staged = fs.mkdtempSync(path.join(stagingRoot, skill.slug + '-'));
|
|
149
|
+
copyTree(extracted.pkgDir, staged, deps.copyDir);
|
|
150
|
+
|
|
151
|
+
if (fs.existsSync(target)) {
|
|
152
|
+
const snapshotRoot = path.join(homeDir, '.yottaskills', 'snapshots', skill.slug);
|
|
153
|
+
fs.mkdirSync(snapshotRoot, { recursive: true });
|
|
154
|
+
snapshot = fs.mkdtempSync(path.join(snapshotRoot, nowTag() + '-' + (existingVersion || 'unknown') + '-'));
|
|
155
|
+
copyTree(target, snapshot, deps.copyDir);
|
|
156
|
+
backup = path.join(dest, '.yottaskills-backup-' + skill.slug + '-' + process.pid);
|
|
157
|
+
renameWithRetry(target, backup);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
renameWithRetry(staged, target);
|
|
161
|
+
staged = null;
|
|
162
|
+
|
|
163
|
+
let evidence;
|
|
164
|
+
try {
|
|
165
|
+
evidence = record({
|
|
166
|
+
event: 'before_install',
|
|
167
|
+
skill: skill.slug,
|
|
168
|
+
package: skill.pkg,
|
|
169
|
+
version: packed.resolved || skill.version,
|
|
170
|
+
gate_mode: gate.mode,
|
|
171
|
+
bootstrap_scan: gate.mode === 'trusted-bootstrap' && skill.slug === 'yotta-verify' ? 'self' : null,
|
|
172
|
+
verdict: scan.verdict,
|
|
173
|
+
decision: scan.decision,
|
|
174
|
+
snapshot,
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
restoreBackup(target, backup);
|
|
178
|
+
backup = null;
|
|
179
|
+
return { skill, status: 'fail', version: packed.resolved, note: '证据写入失败: ' + error.message, exitCode: 1 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (backup && fs.existsSync(backup)) {
|
|
183
|
+
try {
|
|
184
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
185
|
+
} catch (_) {
|
|
186
|
+
// The new version is already active; a stale backup is safer than a failed install.
|
|
187
|
+
}
|
|
188
|
+
backup = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
skill,
|
|
193
|
+
status: 'ok',
|
|
194
|
+
version: packed.resolved || skill.version,
|
|
195
|
+
note: null,
|
|
196
|
+
gate: { ...scan, mode: gate.mode },
|
|
197
|
+
evidence,
|
|
198
|
+
exitCode: 0,
|
|
199
|
+
};
|
|
200
|
+
} catch (error) {
|
|
201
|
+
restoreBackup(target, backup);
|
|
202
|
+
backup = null;
|
|
203
|
+
safeRecord({ event: 'install', skill: skill.slug, decision: 'fail', error: error.message });
|
|
204
|
+
return { skill, status: 'fail', version: null, note: error.message, exitCode: 1 };
|
|
205
|
+
} finally {
|
|
206
|
+
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
207
|
+
if (staged) fs.rmSync(staged, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { createInstaller, renameWithRetry, isSafeTarEntry };
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const MANIFEST_FILE = 'skill-manifest.json';
|
|
6
|
+
|
|
7
|
+
function defaultManifest(skill) {
|
|
8
|
+
return {
|
|
9
|
+
manifestVersion: 1,
|
|
10
|
+
slug: skill.slug,
|
|
11
|
+
name: skill.name,
|
|
12
|
+
package: skill.pkg,
|
|
13
|
+
version: skill.version,
|
|
14
|
+
trust: 'yottameta',
|
|
15
|
+
install: {
|
|
16
|
+
idempotent: true,
|
|
17
|
+
network: 'registry',
|
|
18
|
+
},
|
|
19
|
+
permissions: {
|
|
20
|
+
filesystem: 'user-skills-dir',
|
|
21
|
+
network: 'registry',
|
|
22
|
+
process: 'child-process',
|
|
23
|
+
note: '仅调用本包内安装与自检脚本;默认无自定义脚本。',
|
|
24
|
+
},
|
|
25
|
+
auto_apply: {
|
|
26
|
+
mode: 'route',
|
|
27
|
+
note: '由元阁路由或用户显式调用。',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(file) {
|
|
33
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readSkillVersion(pkgDir) {
|
|
37
|
+
try {
|
|
38
|
+
const text = fs.readFileSync(path.join(pkgDir, 'SKILL.md'), 'utf8');
|
|
39
|
+
const match = text.match(/^version:\s*([0-9]+\.[0-9]+\.[0-9]+)/m);
|
|
40
|
+
return match ? match[1] : null;
|
|
41
|
+
} catch (_) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isSafeRelativePath(value) {
|
|
47
|
+
if (typeof value !== 'string' || value.trim() === '') return false;
|
|
48
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value)) return false;
|
|
49
|
+
const parts = value.split(/[\\/]+/);
|
|
50
|
+
return !parts.includes('..');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function validateManifest(manifest, context) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
const skill = context.skill;
|
|
56
|
+
const pkgDir = context.pkgDir;
|
|
57
|
+
const required = ['manifestVersion', 'slug', 'name', 'package', 'version', 'trust', 'install', 'permissions'];
|
|
58
|
+
for (const key of required) {
|
|
59
|
+
if (manifest[key] === undefined) errors.push('缺少字段: ' + key);
|
|
60
|
+
}
|
|
61
|
+
if (manifest.manifestVersion !== 1) errors.push('manifestVersion 必须为 1');
|
|
62
|
+
if (manifest.slug !== skill.slug) errors.push('slug 与清单不一致');
|
|
63
|
+
if (manifest.package !== skill.pkg) errors.push('package 与清单不一致');
|
|
64
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(String(manifest.slug || ''))) errors.push('slug 格式非法');
|
|
65
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(String(manifest.version || ''))) {
|
|
66
|
+
errors.push('version 格式非法');
|
|
67
|
+
}
|
|
68
|
+
if (!['yottameta', 'third-party-verified', 'unknown'].includes(manifest.trust)) errors.push('trust 非法');
|
|
69
|
+
if (!manifest.install || manifest.install.idempotent !== true) errors.push('install.idempotent 必须为 true');
|
|
70
|
+
if (!manifest.permissions || typeof manifest.permissions !== 'object') errors.push('permissions 缺失');
|
|
71
|
+
if (manifest.permissions && !['none', 'user-skills-dir', 'project-dir', 'user-config', 'other'].includes(manifest.permissions.filesystem)) {
|
|
72
|
+
errors.push('permissions.filesystem 非法');
|
|
73
|
+
}
|
|
74
|
+
if (manifest.permissions && !['none', 'registry', 'allowlist', 'other'].includes(manifest.permissions.network)) {
|
|
75
|
+
errors.push('permissions.network 非法');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let packageMeta = null;
|
|
79
|
+
try {
|
|
80
|
+
packageMeta = readJson(path.join(pkgDir, 'package.json'));
|
|
81
|
+
} catch (error) {
|
|
82
|
+
errors.push('无法读取 package.json: ' + error.message);
|
|
83
|
+
}
|
|
84
|
+
if (packageMeta && packageMeta.name !== skill.pkg) errors.push('package.json.name 与清单不一致');
|
|
85
|
+
if (packageMeta && packageMeta.version !== manifest.version) {
|
|
86
|
+
errors.push('package.json.version 与 manifest.version 不一致');
|
|
87
|
+
}
|
|
88
|
+
const skillVersion = readSkillVersion(pkgDir);
|
|
89
|
+
if (skillVersion !== manifest.version) errors.push('SKILL.md.version 与 manifest.version 不一致');
|
|
90
|
+
for (const key of ['setup', 'doctor', 'rollback']) {
|
|
91
|
+
const rel = manifest.install && manifest.install[key];
|
|
92
|
+
if (rel !== undefined && !isSafeRelativePath(rel)) errors.push('install.' + key + ' 路径不安全');
|
|
93
|
+
}
|
|
94
|
+
return { ok: errors.length === 0, errors };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadManifest(context) {
|
|
98
|
+
const file = path.join(context.pkgDir, MANIFEST_FILE);
|
|
99
|
+
if (!fs.existsSync(file)) {
|
|
100
|
+
return {
|
|
101
|
+
manifest: defaultManifest(context.skill),
|
|
102
|
+
source: 'family-default',
|
|
103
|
+
errors: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let value;
|
|
108
|
+
try {
|
|
109
|
+
value = readJson(file);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
manifest: null,
|
|
113
|
+
source: 'package',
|
|
114
|
+
errors: ['无法解析 skill-manifest.json: ' + error.message],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const validation = validateManifest(value, context);
|
|
118
|
+
return {
|
|
119
|
+
manifest: value,
|
|
120
|
+
source: 'package',
|
|
121
|
+
errors: validation.errors,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
MANIFEST_FILE,
|
|
127
|
+
defaultManifest,
|
|
128
|
+
loadManifest,
|
|
129
|
+
validateManifest,
|
|
130
|
+
isSafeRelativePath,
|
|
131
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const SAFE = 'SAFE TO INSTALL';
|
|
6
|
+
const CAUTION = 'INSTALL WITH CAUTION';
|
|
7
|
+
const REVIEW = 'REVIEW REQUIRED';
|
|
8
|
+
const BLOCK = 'DO NOT INSTALL';
|
|
9
|
+
|
|
10
|
+
function candidateVerifierPaths(dest, opts) {
|
|
11
|
+
const candidates = [];
|
|
12
|
+
if (opts && opts.verify) candidates.push(path.resolve(opts.verify));
|
|
13
|
+
if (process.env.YOTTA_SKILLS_VERIFY) candidates.push(path.resolve(process.env.YOTTA_SKILLS_VERIFY));
|
|
14
|
+
if (dest) candidates.push(path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py'));
|
|
15
|
+
return candidates;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findVerifier(context) {
|
|
19
|
+
const registry = context.registry;
|
|
20
|
+
const candidates = candidateVerifierPaths(context.dest, context.opts);
|
|
21
|
+
if (registry && registry.skills && registry.skills['yotta-verify']) {
|
|
22
|
+
for (const dir of registry.skills['yotta-verify'].source_dirs || []) {
|
|
23
|
+
candidates.push(path.join(dir, 'scripts', 'yotta_verify.py'));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const candidate of candidates) {
|
|
27
|
+
try {
|
|
28
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
29
|
+
} catch (_) {
|
|
30
|
+
// Try the next candidate.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseScanOutput(stdout) {
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(String(stdout || ''));
|
|
39
|
+
if (!value || typeof value.verdict !== 'string') {
|
|
40
|
+
return { ok: false, verdict: null, counts: null, error: '扫描输出缺少 verdict' };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, verdict: value.verdict, counts: value.counts || {}, error: null };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return { ok: false, verdict: null, counts: null, error: error.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function evaluateVerdict(verdict) {
|
|
49
|
+
if (verdict === SAFE) return { decision: 'allow', block: false, warn: false };
|
|
50
|
+
if (verdict === CAUTION || verdict === REVIEW) return { decision: 'warn', block: false, warn: true };
|
|
51
|
+
return { decision: 'block', block: true, warn: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runVerifier(engine, target, options) {
|
|
55
|
+
const result = options.spawnSync(options.python, ['-B', engine, 'scan', target, '--json'], {
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
timeout: 60000,
|
|
58
|
+
});
|
|
59
|
+
if (result && result.error) {
|
|
60
|
+
return { ok: false, verdict: null, counts: null, error: result.error.message, exitCode: null };
|
|
61
|
+
}
|
|
62
|
+
if (!result || result.status === null) {
|
|
63
|
+
return { ok: false, verdict: null, counts: null, error: '元信 scan 执行失败', exitCode: null };
|
|
64
|
+
}
|
|
65
|
+
const parsed = parseScanOutput(result.stdout);
|
|
66
|
+
if (!parsed.ok) {
|
|
67
|
+
return { ok: false, verdict: null, counts: null, error: parsed.error, exitCode: result.status };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, verdict: parsed.verdict, counts: parsed.counts, error: null, exitCode: result.status };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
SAFE,
|
|
74
|
+
CAUTION,
|
|
75
|
+
REVIEW,
|
|
76
|
+
BLOCK,
|
|
77
|
+
findVerifier,
|
|
78
|
+
parseScanOutput,
|
|
79
|
+
evaluateVerdict,
|
|
80
|
+
runVerifier,
|
|
81
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yottameta/yotta-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "YuanGe (元阁) - orchestration routing, inventory, and one-command installer for the YottaMeta skill family (zero dependencies, Node.js only).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
package/references/faq.md
CHANGED
|
@@ -69,11 +69,15 @@ npx -y @yottameta/yotta-skills install yotta-memory yotta-workflow --agent codex
|
|
|
69
69
|
|
|
70
70
|
## 10. 安装前安全扫描怎么用?
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
家族安装默认执行元信门禁:没有元信时先自动自举,再扫描待装包。`DO NOT INSTALL`
|
|
73
|
+
和扫描失败会阻断;`INSTALL WITH CAUTION` / `REVIEW REQUIRED` 会继续但显示风险。
|
|
74
|
+
可用 `--verify <path>` 指定引擎。`--skip-scan` 只是人工应急路径,会标记
|
|
75
|
+
`explicit-unverified`,`update --auto` 不使用它。
|
|
73
76
|
|
|
74
77
|
## 11. 安装失败后如何恢复?
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
查看汇总中的失败原因。旧版本快照保存在 `~/.yottaskills/snapshots/`,当前版本失败时
|
|
80
|
+
新版本不会替换旧版本。先修复目录权限或网络,再单独安装失败技能:
|
|
77
81
|
|
|
78
82
|
```bash
|
|
79
83
|
npx -y @yottameta/yotta-skills install <slug> --dir /path/to/skills
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
1. 读清单(`skills.json`,默认随包;`YOTTA_SKILLS_MANIFEST` 可覆盖);
|
|
11
11
|
2. `npm pack <pkg>@<spec> --pack-destination <临时目录>`;
|
|
12
12
|
3. 系统 `tar -xzf` 解压到临时目录(产物应有 `SKILL.md`);
|
|
13
|
-
4.
|
|
14
|
-
5.
|
|
15
|
-
6.
|
|
16
|
-
7.
|
|
13
|
+
4. 读取包内 `skill-manifest.json`;没有 manifest 时使用 `skills.json` 的家族默认契约;
|
|
14
|
+
5. 校验 slug / package / version / 权限 / 生命周期脚本路径;
|
|
15
|
+
6. 元信装前扫描:已有元信则直接扫描;没有则自动安装元信自身,再扫描;
|
|
16
|
+
7. 通过后创建旧版本快照,把新版本复制到同盘暂存目录,再原子切换到目标目录;
|
|
17
|
+
8. setup / doctor 阶段执行内置检查;P0-2.1 不执行自定义生命周期脚本;
|
|
18
|
+
9. 汇总报告:✔ 成功 / - 跳过(已是最新)/ ✘ 失败;
|
|
19
|
+
10. 全部成功后自动 re-index 本地技能注册表(`~/.yottaskills/registry.json`):重扫技能根目录并增量合并,
|
|
17
20
|
新装 / 更新的技能随即进入注册表(`--no-reindex` 可关闭;`--dry-run` 不触发)。
|
|
18
21
|
|
|
19
22
|
## 复制跳过规则
|
|
@@ -24,6 +27,17 @@
|
|
|
24
27
|
因此装进技能目录的是「技能本体」(SKILL.md / references / scripts 等),不含 npm
|
|
25
28
|
安装器自身、测试夹具与 Python 字节码缓存。
|
|
26
29
|
|
|
30
|
+
## manifest 与家族默认契约
|
|
31
|
+
|
|
32
|
+
每个技能包可以自带根目录 `skill-manifest.json`。有 manifest 时:
|
|
33
|
+
|
|
34
|
+
- 校验 `slug` / `package` / `version` 与包目录、`package.json`、`SKILL.md` 一致;
|
|
35
|
+
- 校验 `install.idempotent` 为 `true`;
|
|
36
|
+
- 校验 setup / doctor / rollback 路径为包内相对路径,不能包含绝对路径或 `..`。
|
|
37
|
+
|
|
38
|
+
没有 manifest 时使用家族默认契约:来源 `yottameta`、幂等安装、自动应用模式 `route`,
|
|
39
|
+
不声明 MCP、hook 或自定义生命周期脚本。
|
|
40
|
+
|
|
27
41
|
## re-index(装技能后自动重扫注册表)
|
|
28
42
|
|
|
29
43
|
- `install` / `update` 完成后,CLI 自动调用 `--reindex`(同一套 `lib/skills-scan.js` 扫描核心):
|
|
@@ -51,14 +65,37 @@
|
|
|
51
65
|
其它 → 直接执行;
|
|
52
66
|
- `YOTTA_SKILLS_NPM_FLAGS` 追加到 `npm pack` 参数。
|
|
53
67
|
|
|
54
|
-
##
|
|
68
|
+
## 元信装前门禁
|
|
55
69
|
|
|
56
70
|
- 引擎查找:`--verify` → `YOTTA_SKILLS_VERIFY` → `<dest>/yotta-verify/scripts/yotta_verify.py`;
|
|
57
71
|
- python 查找:`--python` → `YOTTA_SKILLS_PYTHON` → `python3` / `python` / `py`(win32);
|
|
58
72
|
- 执行:`python -B <engine> scan <解压目录> --json`(`-B` 禁止写 `__pycache__`,防止污染
|
|
59
73
|
引擎所在目录);
|
|
60
|
-
-
|
|
61
|
-
|
|
74
|
+
- 元信缺失时:先按 `skills.json` 安装 `yotta-verify`,并记录 `gate_mode=trusted-bootstrap`
|
|
75
|
+
与 `bootstrap_scan=self`,随后用元信扫描其余家族包;
|
|
76
|
+
- verdict 处置:`SAFE TO INSTALL` 继续;`INSTALL WITH CAUTION` / `REVIEW REQUIRED`
|
|
77
|
+
继续但显示风险并留证;`DO NOT INSTALL` 和扫描失败阻断,不替换目标;
|
|
78
|
+
- `--skip-scan` 只保留为人工应急路径,使用时输出 `explicit-unverified` 并写入证据;
|
|
79
|
+
- `update --auto` 始终执行装前门禁,不接受 `--skip-scan`。
|
|
80
|
+
|
|
81
|
+
## 快照与回滚
|
|
82
|
+
|
|
83
|
+
- 旧版本快照:`~/.yottaskills/snapshots/<slug>/<timestamp>-<version>-<随机后缀>/`;
|
|
84
|
+
- 新版本先落在 `<dest>/.yottaskills-staging/<slug>-<随机后缀>/`;
|
|
85
|
+
- 旧目标先重命名为同盘备份,再把暂存目录切到目标;切换失败时恢复备份;
|
|
86
|
+
- 证据写入失败会触发回滚,不把安装标记为成功;
|
|
87
|
+
- Windows 上对 `EPERM` / `EACCES` / `EBUSY` 做短重试,避免杀毒或索引服务造成的瞬时锁。
|
|
88
|
+
|
|
89
|
+
## 安装证据
|
|
90
|
+
|
|
91
|
+
每次安装决策写入:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
~/.yottaskills/install-log.jsonl
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
记录包含时间、事件、技能、包名、版本、`gate_mode`、verdict、decision
|
|
98
|
+
和快照路径;不记录技能内容或用户数据,只写本机。
|
|
62
99
|
|
|
63
100
|
## 退出码
|
|
64
101
|
|
|
@@ -68,6 +105,8 @@
|
|
|
68
105
|
| 1 | 安装 / 更新存在失败项 |
|
|
69
106
|
| 2 | 用法错误(未知参数 / 未知技能 / 未收录智能体) |
|
|
70
107
|
| 4 | 未指定目标且当前目录未检测到项目级技能目录 |
|
|
108
|
+
| 5 | 装前 gate 阻断或元信不可用 |
|
|
109
|
+
| 6 | manifest / 身份校验失败 |
|
|
71
110
|
|
|
72
111
|
## 目标目录解析
|
|
73
112
|
|
package/references/skill-list.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| `yotta-anti-shallow` | 元谨 | `@yottameta/yotta-anti-shallow` | 1.4.0 | 防 AI 敷衍规则引擎(深入分析/根因追溯时激活) |
|
|
9
9
|
| `yotta-code-quality` | 元质 | `@yottameta/yotta-code-quality` | 0.4.0 | 结对式代码质量评审(十二类腐化风险 + 0-100 健康分) |
|
|
10
10
|
| `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.0 | 跨会话/跨项目通用工作流协议(状态就近存) |
|
|
11
|
-
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.
|
|
11
|
+
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.12.1 | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
|
|
12
12
|
| `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.2.0 | 学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目 |
|
|
13
13
|
| `yotta-security-audit` | 元安 | `@yottameta/yotta-security-audit` | 0.2.2 | 安全扫描引擎:13 类检测器 + 系统安全基线 |
|
|
14
14
|
| `yotta-vetter` | 元审 | `@yottameta/yotta-vetter` | 0.2.3 | 安全审查协议:四阶段 review + SAFE TO INSTALL 判定 |
|
package/references/tutorial.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- 学会 `--list` / `install` / `update` / `--dry-run` / `--pin` / `--force` 的用法;
|
|
10
10
|
- 理解版本策略(range 与 --pin)与幂等行为;
|
|
11
|
-
-
|
|
11
|
+
- 了解元信装前门禁、元信自举与 `--skip-scan` 的人工应急边界;
|
|
12
12
|
- 前置:Node.js 18+、npm、系统 tar(Windows 10+ 自带);网络可访问 npm 注册表
|
|
13
13
|
(国内可配置镜像或代理)。
|
|
14
14
|
|
|
@@ -76,22 +76,26 @@ dry-run 只打印将要执行的清单(含目标与版本策略),不联网
|
|
|
76
76
|
npx -y @yottameta/yotta-skills install --agent codex --pin
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
## 8.
|
|
79
|
+
## 8. 强制重装与元信门禁
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
82
|
npx -y @yottameta/yotta-skills install --agent codex --force # 已是最新也重装
|
|
83
|
-
npx -y @yottameta/yotta-skills install --agent codex --skip-scan #
|
|
83
|
+
npx -y @yottameta/yotta-skills install --agent codex --skip-scan # 人工应急:显式未验证
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
家族安装默认执行元信门禁:没有元信时,元阁先按可信源安装元信自身,再扫描待装技能。
|
|
87
|
+
`DO NOT INSTALL` 会阻断安装;`INSTALL WITH CAUTION` / `REVIEW REQUIRED` 会继续但显示风险。
|
|
88
|
+
旧版本会先快照到 `~/.yottaskills/snapshots/`,安装决策写入
|
|
89
|
+
`~/.yottaskills/install-log.jsonl`。`--skip-scan` 只用于人工应急,会输出
|
|
90
|
+
`explicit-unverified` 并留证,`update --auto` 不会使用它。
|
|
88
91
|
|
|
89
92
|
## 9. 验证安装结果
|
|
90
93
|
|
|
91
94
|
- 看安装汇总:成功 N / 跳过 N / 失败 N;
|
|
92
95
|
- 抽查技能目录:`ls ~/.codex/skills/yotta-memory/SKILL.md`;
|
|
93
96
|
- 复核版本:每个技能 SKILL.md 的 frontmatter `version` 应与 `--list` 一致(这也是幂等
|
|
94
|
-
|
|
97
|
+
判断的依据);
|
|
98
|
+
- 需要安全复核时,查看 `~/.yottaskills/install-log.jsonl` 中的 verdict 与 `gate_mode`。
|
|
95
99
|
|
|
96
100
|
## 10. 常见问题
|
|
97
101
|
|