@yottameta/yotta-skills 0.7.1 → 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 CHANGED
@@ -1,3 +1,22 @@
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
+
12
+ ## v0.7.2 (2026-09-10)
13
+
14
+ skills.json 清单同步(元序路径模型澄清升版后,元阁安装清单随包更新):
15
+
16
+ - yotta-workflow 0.3.0 → 0.4.0。
17
+ - skills.json 与 references/skill-list.md 同步,updated → 2026-09-10。
18
+ - 版本四件对齐 0.7.2;无功能代码变更。
19
+
1
20
  ## v0.7.1 (2026-09-09)
2
21
 
3
22
  skills.json 清单同步(评测批 2 六技能升版后,元阁安装清单随包更新):
@@ -34,7 +53,6 @@ skills.json 清单同步(评测批 2 六技能升版后,元阁安装清单
34
53
  ## v0.5.1 (2026-09-06)
35
54
 
36
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 + 技能分发清单副本 + 插件同步。
37
- # 更新日志
38
56
 
39
57
  ## v0.5.0 (2026-08-31)
40
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; quick to run at session start); <code>update --auto</code> upgrades the installed YottaMeta family to the latest (non-family skills are never auto-updated).
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 summary** — if yotta-verify (元信) is available, each skill is scanned first; the verdict is informational only.
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. It never writes outside the target you specify.
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 summary applies; non-family skills untouched)
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 summary; never auto-updates non-family skills) |
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` | Skip the yotta-verify pre-install summary (auto-enabled when yotta-verify is installed) |
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 the yotta-verify summary) |
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` → optional yotta-verify pre-install summary → 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`.
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 注册表并报告(不改动,适合会话开工快速跑);<code>update --auto</code> 检查后自动把已装元阁家族升到最新(非元阁家族技能绝不自动更新)。
35
+ - **更新检查 / 自动更新**——<code>update --check</code> 只读比对已装技能版本与 npm 注册表并报告(不改动,手动或由 doctor / 发布前检查触发);<code>update --auto</code> 检查后自动把已装元阁家族升到最新(非元阁家族技能绝不自动更新)。
36
36
  - **幂等**——已在清单版本的技能跳过;重复运行安全。
37
- - **装前摘要**——若装了元信(yotta-verify),先对每个待装技能做一次装前扫描;verdict 仅提示、不拦截。
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` | 跳过元信装前摘要(装了 yotta-verify 时默认自动启用) |
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
- 替换 `<dest>/<slug>` → 汇总报告。`install` / `update` 完成后自动重扫本地技能注册表,
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`、异常路径与元信 scan 集成
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.7.1
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` | 跳过元信装前摘要(装了 yotta-verify 时默认自动启用) |
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`。可用 `--skip-scan` 关闭。
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 可执行文件(元信摘要用,同 `--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
 
@@ -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(Object.assign({}, row.family, { version: row.latest }), dest, Object.assign({}, opts, { force: true, pin: true }));
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 failed2++;
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 ? 1 : 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 cands = [];
405
- if (opts.verify) cands.push(path.resolve(opts.verify));
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, note: '未找到 python(元信 scan 需要 Python 3.8+)' };
430
- // -B:禁止 Python 写 __pycache__(否则会在引擎所在目录(可能是已装技能)留下 .pyc 污染)
431
- const r = spawnSync(python, ['-B', engine, 'scan', skillDir, '--json'], { encoding: 'utf8', timeout: 60000 });
432
- if (r.status === null) return { ok: false, note: '元信 scan 执行失败' };
433
- try {
434
- const j = JSON.parse(r.stdout);
435
- const c = j.counts || {};
436
- return { ok: true, verdict: j.verdict, counts: c };
437
- } catch (_) {
438
- return { ok: false, note: '元信 scan 输出无法解析' };
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 installOne(skill, dest, opts) {
481
- const target = path.join(dest, skill.slug);
482
- const existing = readInstalledVersion(target);
483
- if (!opts.force && existing === skill.version) {
484
- return { skill, status: 'skip', version: existing, note: '已是最新' };
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-skills-'));
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(skill, opts, packDir);
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 extracted = extractTarball(packed.tarball, extractDir);
499
- if (extracted.error) return { skill, status: 'fail', version: packed.resolved, note: extracted.error };
500
-
501
- // 元信 scan(若可用):装前摘要,不拦截
502
- if (!opts.skipScan) {
503
- const engine = findVerifyEngine(dest, opts);
504
- if (engine) {
505
- const scan = runScan(engine, extracted.pkgDir);
506
- if (scan.ok) {
507
- const c = scan.counts;
508
- const line = 'critical ' + (c.critical || 0) + ' / high ' + (c.high || 0) + ' / medium ' + (c.medium || 0) +
509
- ' / low ' + (c.low || 0) + ' / info ' + (c.info || 0);
510
- out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
511
- if (scan.verdict && /DO NOT INSTALL/.test(scan.verdict)) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,请人工复核后再使用。');
512
- } else {
513
- out(' 元信 scan: ' + scan.note);
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') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
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 { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
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') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
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 { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
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 };
@@ -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.7.1",
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
- 默认会查找可用的元信引擎并输出摘要;可用 `--verify <path>` 指定引擎,或 `--skip-scan` 关闭。扫描结果仅提示,不替代人工判断。
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. 元信装前摘要(若引擎可用;`--skip-scan` 关闭);
14
- 5. 删除目标 `<dest>/<slug>` 后重新复制(跳过规则见下);
15
- 6. 汇总报告:✔ 成功 / - 跳过(已是最新)/ ✘ 失败;有失败项时退出码 1。
16
- 7. 全部完成后自动 re-index 本地技能注册表(`~/.yottaskills/registry.json`):重扫技能根目录并增量合并,
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
- - 输出 verdict + counts(critical / high / medium / low / info);DO NOT INSTALL 额外提示
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
 
@@ -7,8 +7,8 @@
7
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
- | `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.3.0 | 跨会话/跨项目通用工作流协议(状态就近存) |
11
- | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.11.0 | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
10
+ | `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.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 判定 |
@@ -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
- 元信(yotta-verify)已安装时默认会对每个待装技能输出装前摘要(verdict + 计数),仅提示
87
- 不拦截;verdict 为 DO NOT INSTALL 时会提示人工复核。
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
 
@@ -24,7 +24,7 @@ import subprocess
24
24
  import sys
25
25
  from pathlib import Path
26
26
 
27
- VERSION = "0.7.1"
27
+ VERSION = "0.8.0"
28
28
  TOOL_NAME = "yotta-skills"
29
29
  CN_NAME = "元阁"
30
30
  MCP_PROTOCOL_MODERN = "2026-07-28"
package/skills.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifestVersion": 1,
3
- "updated": "2026-09-09",
3
+ "updated": "2026-09-10",
4
4
  "note": "清单同步要求:每次技能发布新版本后,按 YottaSkills 技能登记表更新本文件(登记表为内部台账,不随包分发)。",
5
5
  "skills": [
6
6
  {
@@ -21,14 +21,14 @@
21
21
  "slug": "yotta-workflow",
22
22
  "name": "元序",
23
23
  "pkg": "@yottameta/yotta-workflow",
24
- "version": "0.3.0",
24
+ "version": "0.4.0",
25
25
  "desc": "跨会话/跨项目通用工作流协议(状态就近存)"
26
26
  },
27
27
  {
28
28
  "slug": "yotta-memory",
29
29
  "name": "元忆",
30
30
  "pkg": "@yottameta/yotta-memory",
31
- "version": "0.11.0",
31
+ "version": "0.12.1",
32
32
  "desc": "有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT)"
33
33
  },
34
34
  {