release-skill 0.2.6 → 0.2.8
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +42 -0
- package/INSTALL.md +12 -12
- package/INSTALL.zh-CN.md +10 -10
- package/README.md +26 -11
- package/README.zh-CN.md +22 -11
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +2619 -1728
- package/adapters/claude/schemas/.render-manifest.json +2 -2
- package/adapters/claude/schemas/release-project.schema.json +60 -0
- package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
- package/adapters/claude/skills/release-setup/SKILL.md +10 -1
- package/adapters/claude/skills/release-verify/SKILL.md +4 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2619 -1728
- package/adapters/codex/schemas/.render-manifest.json +2 -2
- package/adapters/codex/schemas/release-project.schema.json +60 -0
- package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
- package/adapters/codex/skills/release-setup/SKILL.md +10 -1
- package/adapters/codex/skills/release-verify/SKILL.md +4 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2619 -1728
- package/adapters/kimi/schemas/.render-manifest.json +2 -2
- package/adapters/kimi/schemas/release-project.schema.json +60 -0
- package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +2619 -1728
- package/adapters/workbuddy/schemas/.render-manifest.json +2 -2
- package/adapters/workbuddy/schemas/release-project.schema.json +60 -0
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
- package/bin/release-skill.bundle.mjs +2619 -1728
- package/package.json +1 -1
- package/schemas/.render-manifest.json +2 -2
- package/schemas/release-project.schema.json +60 -0
- package/skills/release-prepare/SKILL.md +5 -0
- package/skills/release-setup/SKILL.md +10 -1
- package/skills/release-verify/SKILL.md +4 -2
- package/skills-src/release-prepare/SKILL.md +5 -0
- package/skills-src/release-setup/SKILL.md +10 -1
- package/skills-src/release-verify/SKILL.md +4 -2
- package/src/adapters/npm.mjs +54 -2
- package/src/adapters/plugin-marketplace.mjs +4 -29
- package/src/commands/prepare.mjs +49 -3
- package/src/commands/publish.mjs +2 -2
- package/src/commands/reconcile.mjs +39 -0
- package/src/commands/setup.mjs +168 -0
- package/src/commands/verify.mjs +28 -1
- package/src/core/config.mjs +48 -0
- package/src/core/public-surface.mjs +509 -0
- package/src/npm/npm-entry-closure.mjs +195 -0
- package/src/platforms/codebuddy.mjs +19 -11
- package/src/platforms/codex.mjs +18 -10
- package/src/platforms/kimi.mjs +26 -39
- package/src/snapshot/frozen.mjs +51 -19
package/package.json
CHANGED
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"bytes": 35256
|
|
27
27
|
},
|
|
28
28
|
"release-project.schema.json": {
|
|
29
|
-
"digest": "
|
|
30
|
-
"bytes":
|
|
29
|
+
"digest": "7ac18070ac41327a330ca35e32f466e3f2a8eb2b7df3a5755568e00f991ffdc1",
|
|
30
|
+
"bytes": 32497
|
|
31
31
|
},
|
|
32
32
|
"release-run.schema.json": {
|
|
33
33
|
"digest": "bf3c8fb4ca2fc078272a45a6ee6bf663fd2420a722b2f995dbaabf2635e2179e",
|
|
@@ -455,6 +455,9 @@
|
|
|
455
455
|
]
|
|
456
456
|
}
|
|
457
457
|
},
|
|
458
|
+
"expectedPublicSurface": {
|
|
459
|
+
"$ref": "#/definitions/expectedPublicSurface"
|
|
460
|
+
},
|
|
458
461
|
"publicFiles": {
|
|
459
462
|
"type": "array",
|
|
460
463
|
"minItems": 1,
|
|
@@ -761,6 +764,63 @@
|
|
|
761
764
|
}
|
|
762
765
|
},
|
|
763
766
|
"definitions": {
|
|
767
|
+
"expectedPublicSurface": {
|
|
768
|
+
"type": "object",
|
|
769
|
+
"additionalProperties": false,
|
|
770
|
+
"required": [
|
|
771
|
+
"scanRoots"
|
|
772
|
+
],
|
|
773
|
+
"properties": {
|
|
774
|
+
"scanRoots": {
|
|
775
|
+
"type": "array",
|
|
776
|
+
"minItems": 1,
|
|
777
|
+
"items": {
|
|
778
|
+
"$ref": "#/definitions/publicSurfaceScanRoot"
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
"publicSurfaceScanRoot": {
|
|
784
|
+
"type": "object",
|
|
785
|
+
"additionalProperties": false,
|
|
786
|
+
"required": [
|
|
787
|
+
"include",
|
|
788
|
+
"exclude"
|
|
789
|
+
],
|
|
790
|
+
"properties": {
|
|
791
|
+
"sourceScope": {
|
|
792
|
+
"type": "string",
|
|
793
|
+
"enum": [
|
|
794
|
+
"unit",
|
|
795
|
+
"workspace"
|
|
796
|
+
],
|
|
797
|
+
"default": "unit"
|
|
798
|
+
},
|
|
799
|
+
"root": {
|
|
800
|
+
"type": "string",
|
|
801
|
+
"minLength": 1,
|
|
802
|
+
"default": ".",
|
|
803
|
+
"pattern": "^(?:\\.$|(?!\\/)(?!\\.\\/)(?!.*\\\\)(?!.*:)(?!\\.\\.(?:\\/|$))(?!.*\\/\\.\\.\\/)(?!.*\\/\\.\\.$)(?!.*\\/\\.\\/)(?!.*\\/\\/)(?!.*\\0)(?!.*\\/\\.$)(?!.*\\/\\/$)(?!.*\\/$).*)$"
|
|
804
|
+
},
|
|
805
|
+
"include": {
|
|
806
|
+
"type": "array",
|
|
807
|
+
"minItems": 1,
|
|
808
|
+
"uniqueItems": true,
|
|
809
|
+
"items": {
|
|
810
|
+
"type": "string",
|
|
811
|
+
"minLength": 1
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
"exclude": {
|
|
815
|
+
"type": "array",
|
|
816
|
+
"uniqueItems": true,
|
|
817
|
+
"items": {
|
|
818
|
+
"type": "string",
|
|
819
|
+
"minLength": 1
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
},
|
|
764
824
|
"hook": {
|
|
765
825
|
"type": "object",
|
|
766
826
|
"required": [
|
|
@@ -36,6 +36,10 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
36
36
|
|
|
37
37
|
若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
|
|
38
38
|
Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
|
|
39
|
+
每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
|
|
40
|
+
`bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
|
|
41
|
+
该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
|
|
42
|
+
猜测展开;它与 fallback array 都属于首版最小边界外的阻断形态。
|
|
39
43
|
每个 release unit 必须显式配置 `previousPublicBaseline`。只有确认不存在前序公开
|
|
40
44
|
版本时用 `mode: none`;已有版本必须用 `mode: bound` + 精确 repo/ref/commit,并以
|
|
41
45
|
`--online --production` 逐 unit 观察 ref→commit mapping。默认 observer 不下载远端
|
|
@@ -79,6 +83,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offli
|
|
|
79
83
|
| GATE_FAILED (gate 授权) | 向用户展示 snapshot gate 命令和风险,获得授权后加 `--acknowledge-gate-side-effects` 重试 |
|
|
80
84
|
| GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
|
|
81
85
|
| GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
|
|
86
|
+
| GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
|
|
82
87
|
| GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
|
|
83
88
|
| RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
|
|
84
89
|
| RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
|
|
@@ -14,6 +14,9 @@ description: "首次接入 release-skill:只读发现发布单元与个性化
|
|
|
14
14
|
- 默认只读。README、slogan、CHANGELOG、业务脚本和已有配置均为人工权威内容,不得重写或覆盖。
|
|
15
15
|
- 发现脚本不等于选择或授权。间接脚本以 `SIDE_EFFECTS_UNPROVEN` 排除;项目特有 hook/gate 只能人工增量注册。
|
|
16
16
|
- 仓库、公开文件等证据冲突时停止自动提案,交给人工修正权威事实后重新发现。
|
|
17
|
+
- npm 单元只读提取 `package.json` 的具体入口和旧 `npmRequiredPackagePaths`,报告
|
|
18
|
+
tracked/untracked/ignored/missing/non-regular 状态与覆盖漂移;这些是人工复核候选,
|
|
19
|
+
不得自动写入 `publicFiles` 或 `requiredPublicFiles`。
|
|
17
20
|
- 写入只允许 create-once(仅创建一次),必须同时提供 answers 与精确 `setupDigest`;无安全原生写入能力时失败关闭。
|
|
18
21
|
- Agent 的多次 shell 调用彼此独立;只能用首轮打印的会话目录绝对路径续接,不得假设变量仍存在。
|
|
19
22
|
|
|
@@ -43,7 +46,13 @@ node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.
|
|
|
43
46
|
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
若 `proposalConflicts`
|
|
49
|
+
若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实或 npm 入口候选,
|
|
50
|
+
删除本会话目录后从第 2 步重跑,不得猜测选边。构建后才产生且被忽略的入口应先由
|
|
51
|
+
人工确认构建流程与最终 tarball 边界,不能因为 setup 发现了路径就自动信任或纳入公开
|
|
52
|
+
配置。无冲突时可人工增量编辑 `answers.json`:hook 写入
|
|
53
|
+
`recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入
|
|
54
|
+
`verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持
|
|
55
|
+
`mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
|
|
47
56
|
|
|
48
57
|
3. 使用上一步打印的两个绝对字面量重新赋值,运行绑定 dry-run;任何人工编辑后都必须重跑本步:
|
|
49
58
|
|
|
@@ -17,7 +17,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
|
|
|
17
17
|
|
|
18
18
|
## 职责与边界
|
|
19
19
|
|
|
20
|
-
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm
|
|
20
|
+
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
|
|
21
21
|
|
|
22
22
|
**阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
|
|
23
23
|
|
|
@@ -55,8 +55,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
|
|
|
55
55
|
- 在 `os.tmpdir()` 创建隔离目录
|
|
56
56
|
- 执行 `npm install <package>@<version>` 带安全标志
|
|
57
57
|
- 验证安装的 `package.json` name 和 version 精确匹配
|
|
58
|
+
- 无条件静态验证已安装包中具体 `bin`/`main`/`module`/`types`/`typings`/`exports`
|
|
59
|
+
目标是普通文件;失败时不得写入 `VERIFIED`
|
|
58
60
|
- 若配置了 `smokeBin`:验证 bin 路径安全(无逃逸、无 symlink),从精确安装根、隔离 HOME 和最小环境执行并验证输出;这会运行已安装代码,必须显式授权
|
|
59
|
-
- 若未配置 `smokeBin
|
|
61
|
+
- 若未配置 `smokeBin`:跳过运行代码,但静态入口闭包检查仍然强制执行
|
|
60
62
|
|
|
61
63
|
## 常见错误
|
|
62
64
|
|
|
@@ -36,6 +36,10 @@ description: Freeze an immutable release plan with local configuration, document
|
|
|
36
36
|
|
|
37
37
|
若用户明确要求 GitHub+npm 生产发布,加入 `--production`。该模式还会封存独立
|
|
38
38
|
Git commit/tree 和 npm tarball,并把路径、SHA/integrity、branch/tag 写入计划。
|
|
39
|
+
每个 npm tarball 在计划落盘前必须静态验证 `package.json` 的具体
|
|
40
|
+
`bin`/`main`/`module`/`types`/`typings`/`exports` 入口均为 tarball 内普通文件;
|
|
41
|
+
该门禁不依赖项目是否配置 `requiredPublicFiles` 或 `smokeBin`。通配符 exports 不做
|
|
42
|
+
猜测展开;它与 fallback array 都属于首版最小边界外的阻断形态。
|
|
39
43
|
每个 release unit 必须显式配置 `previousPublicBaseline`。只有确认不存在前序公开
|
|
40
44
|
版本时用 `mode: none`;已有版本必须用 `mode: bound` + 精确 repo/ref/commit,并以
|
|
41
45
|
`--online --production` 逐 unit 观察 ref→commit mapping。默认 observer 不下载远端
|
|
@@ -79,6 +83,7 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" prepare --root <path> --offli
|
|
|
79
83
|
| GATE_FAILED (gate 授权) | 向用户展示 snapshot gate 命令和风险,获得授权后加 `--acknowledge-gate-side-effects` 重试 |
|
|
80
84
|
| GATE_FAILED (bound + offline) | 改用 `--online --production`,不得把 unobserved-offline plan 交给 publish |
|
|
81
85
|
| GATE_FAILED (前序基线漂移) | 先取得并比较实际远端内容;人工选择 merge/adopt/reject。merge/adopt 都必须把接受内容落回 human-owned 权威源,并把 `previousPublicBaseline` 更新为接受状态的精确 repo/ref/commit 后重新 online production prepare;reject 停止调查,禁止改 `mode: none` 绕过 |
|
|
86
|
+
| GATE_FAILED (`npm-entry-closure`) | 修复打包内容或入口声明后重新 prepare;不得用 `requiredPublicFiles`/`smokeBin` 缺省绕过 |
|
|
82
87
|
| GATE_FAILED (其他) | 修复门失败原因后重试;以 CLI exit code 为准 |
|
|
83
88
|
| RELEASE_DOCS_STALE | 文档相对说明源已陈旧;按详情运行只读演练,展示文件/语种/版本/摘要,经用户授权“本地发布文档写入”后执行写入,审阅提交再重新 prepare |
|
|
84
89
|
| RELEASE_DOCS_INVALID / TRANSLATION_MISSING / CONFLICT / REFRESH_STALE | 修复配置/说明源/目标或重新演练取得新 `refreshDigest`;不得扩大写入范围绕过 |
|
|
@@ -14,6 +14,9 @@ description: "首次接入 release-skill:只读发现发布单元与个性化
|
|
|
14
14
|
- 默认只读。README、slogan、CHANGELOG、业务脚本和已有配置均为人工权威内容,不得重写或覆盖。
|
|
15
15
|
- 发现脚本不等于选择或授权。间接脚本以 `SIDE_EFFECTS_UNPROVEN` 排除;项目特有 hook/gate 只能人工增量注册。
|
|
16
16
|
- 仓库、公开文件等证据冲突时停止自动提案,交给人工修正权威事实后重新发现。
|
|
17
|
+
- npm 单元只读提取 `package.json` 的具体入口和旧 `npmRequiredPackagePaths`,报告
|
|
18
|
+
tracked/untracked/ignored/missing/non-regular 状态与覆盖漂移;这些是人工复核候选,
|
|
19
|
+
不得自动写入 `publicFiles` 或 `requiredPublicFiles`。
|
|
17
20
|
- 写入只允许 create-once(仅创建一次),必须同时提供 answers 与精确 `setupDigest`;无安全原生写入能力时失败关闭。
|
|
18
21
|
- Agent 的多次 shell 调用彼此独立;只能用首轮打印的会话目录绝对路径续接,不得假设变量仍存在。
|
|
19
22
|
|
|
@@ -43,7 +46,13 @@ node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.
|
|
|
43
46
|
node -e 'const fs=require("node:fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));if((r.proposalConflicts??[]).length){console.error("proposal conflicts require human resolution");process.exit(2)}if(!r.recommendedAnswers){console.error("recommendedAnswers missing");process.exit(2)}fs.writeFileSync(process.argv[2],JSON.stringify(r.recommendedAnswers,null,2)+"\n",{flag:"wx",mode:0o600})' "$REPORT" "$ANSWERS"
|
|
44
47
|
```
|
|
45
48
|
|
|
46
|
-
若 `proposalConflicts`
|
|
49
|
+
若 `proposalConflicts` 非空,暂停,让用户修正仓库/映射权威事实或 npm 入口候选,
|
|
50
|
+
删除本会话目录后从第 2 步重跑,不得猜测选边。构建后才产生且被忽略的入口应先由
|
|
51
|
+
人工确认构建流程与最终 tarball 边界,不能因为 setup 发现了路径就自动信任或纳入公开
|
|
52
|
+
配置。无冲突时可人工增量编辑 `answers.json`:hook 写入
|
|
53
|
+
`recommendedAnswers.projectConfig.hooks` 对应的 `projectConfig.hooks`;gate 写入
|
|
54
|
+
`verificationGates`,并把同一 id 加入 `selectedGateIds`。人工文件保持
|
|
55
|
+
`mode: preserve`,跨单元共享源使用 `sourceScope: workspace`。
|
|
47
56
|
|
|
48
57
|
3. 使用上一步打印的两个绝对字面量重新赋值,运行绑定 dry-run;任何人工编辑后都必须重跑本步:
|
|
49
58
|
|
|
@@ -17,7 +17,7 @@ verify 只接受 `PUBLISHED` 状态的源 run;`VERIFIED` 是终态,不会再
|
|
|
17
17
|
|
|
18
18
|
## 职责与边界
|
|
19
19
|
|
|
20
|
-
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm
|
|
20
|
+
验证远端所有 action 的实际状态与冻结计划一致。执行精确 `<package>@<version>` npm 安装到隔离目录,验证包名、版本、静态入口闭包,并在配置时验证 bin 路径安全和 CLI 烟雾输出。对每个声明的 marketplace distribution 执行全新隔离消费者安装验证。
|
|
21
21
|
|
|
22
22
|
**阶段通过规则**: 只有 CLI exit code 0 和结构化状态码 `VERIFIED` 才是完整终态。
|
|
23
23
|
|
|
@@ -55,8 +55,10 @@ node "${CLAUDE_PLUGIN_ROOT}/bin/release-skill.mjs" verify --root <path> --plan <
|
|
|
55
55
|
- 在 `os.tmpdir()` 创建隔离目录
|
|
56
56
|
- 执行 `npm install <package>@<version>` 带安全标志
|
|
57
57
|
- 验证安装的 `package.json` name 和 version 精确匹配
|
|
58
|
+
- 无条件静态验证已安装包中具体 `bin`/`main`/`module`/`types`/`typings`/`exports`
|
|
59
|
+
目标是普通文件;失败时不得写入 `VERIFIED`
|
|
58
60
|
- 若配置了 `smokeBin`:验证 bin 路径安全(无逃逸、无 symlink),从精确安装根、隔离 HOME 和最小环境执行并验证输出;这会运行已安装代码,必须显式授权
|
|
59
|
-
- 若未配置 `smokeBin
|
|
61
|
+
- 若未配置 `smokeBin`:跳过运行代码,但静态入口闭包检查仍然强制执行
|
|
60
62
|
|
|
61
63
|
## 常见错误
|
|
62
64
|
|
package/src/adapters/npm.mjs
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
createResult,
|
|
17
17
|
matchObservation,
|
|
18
18
|
} from './contract.mjs';
|
|
19
|
+
import {
|
|
20
|
+
checkNpmEntryClosure,
|
|
21
|
+
buildTarballFileIndex,
|
|
22
|
+
} from '../npm/npm-entry-closure.mjs';
|
|
23
|
+
import { ReleaseError, GATE_FAILED } from '../core/errors.mjs';
|
|
19
24
|
|
|
20
25
|
const execFile = promisify(execFileCb);
|
|
21
26
|
const NAME = 'npm';
|
|
@@ -349,6 +354,53 @@ export async function verifyFrozenNpmTarballIdentity(action, root) {
|
|
|
349
354
|
});
|
|
350
355
|
}
|
|
351
356
|
|
|
357
|
+
async function verifyNpmTarballBufferContract(buffer, action, tarballDir) {
|
|
358
|
+
const manifest = extractManifestFromTarball(buffer, {
|
|
359
|
+
name: action.package,
|
|
360
|
+
version: action.version,
|
|
361
|
+
});
|
|
362
|
+
const fileIndex = await buildTarballFileIndex(buffer, tarballDir);
|
|
363
|
+
const closureResult = checkNpmEntryClosure(manifest, fileIndex);
|
|
364
|
+
if (closureResult.errors.length > 0) {
|
|
365
|
+
throw new ReleaseError(
|
|
366
|
+
GATE_FAILED,
|
|
367
|
+
`npm entry closure check failed for ${action.package}@${action.version}: ${closureResult.errors.length} error(s)`,
|
|
368
|
+
{
|
|
369
|
+
gate: 'npm-entry-closure',
|
|
370
|
+
package: action.package,
|
|
371
|
+
version: action.version,
|
|
372
|
+
entries: closureResult.entries,
|
|
373
|
+
errors: closureResult.errors,
|
|
374
|
+
diagnostics: closureResult.diagnostics,
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return { manifest, closureResult };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Shared frozen tarball contract helper: reads the verified tarball bytes ONCE
|
|
383
|
+
* and performs both identity verification (name/version from manifest) and
|
|
384
|
+
* entry closure validation (bin/main/module/types/typings/exports targets
|
|
385
|
+
* exist as regular files inside the tarball).
|
|
386
|
+
*
|
|
387
|
+
* The same Buffer that passes `readVerifiedTarballBytes` is reused for both
|
|
388
|
+
* the existing identity reader and the controlled extraction/file-index
|
|
389
|
+
* boundary. This adds no new tar parser and performs no second source read.
|
|
390
|
+
*
|
|
391
|
+
* @param {object} action - The frozen action with tarballPath, tarballSha256,
|
|
392
|
+
* integrity, package, and version.
|
|
393
|
+
* @param {string} root - Absolute project root.
|
|
394
|
+
* @param {string} [tarballDir] - Temp directory for tar extraction (defaults to os.tmpdir()).
|
|
395
|
+
* @returns {Promise<{ manifest: object, closureResult: object }>}
|
|
396
|
+
* @throws {ReleaseError} GATE_FAILED with details.gate='npm-entry-closure'
|
|
397
|
+
* when any declared entry is missing or invalid.
|
|
398
|
+
*/
|
|
399
|
+
export async function verifyFrozenNpmTarballContract(action, root, tarballDir) {
|
|
400
|
+
const buffer = await readVerifiedTarballBytes(action, root);
|
|
401
|
+
return verifyNpmTarballBufferContract(buffer, action, tarballDir);
|
|
402
|
+
}
|
|
403
|
+
|
|
352
404
|
/**
|
|
353
405
|
* Read and verify the frozen tarball, then write it to a controlled named
|
|
354
406
|
* temp file under a temp directory adjacent to the source. Returns the temp
|
|
@@ -700,7 +752,7 @@ export function createNpmAdapter(deps = {}) {
|
|
|
700
752
|
}
|
|
701
753
|
const cwd = resolvePackageCwd(action.cwd, context.root);
|
|
702
754
|
if (action.tarballPath) {
|
|
703
|
-
await
|
|
755
|
+
await verifyFrozenNpmTarballContract(action, context.root);
|
|
704
756
|
}
|
|
705
757
|
|
|
706
758
|
let whoamiUser;
|
|
@@ -775,7 +827,7 @@ export function createNpmAdapter(deps = {}) {
|
|
|
775
827
|
throw new Error('frozen npm tarball requires a stable Buffer publish capability');
|
|
776
828
|
}
|
|
777
829
|
const buffer = await readVerifiedTarballBytes(action, context.root);
|
|
778
|
-
const manifest =
|
|
830
|
+
const { manifest } = await verifyNpmTarballBufferContract(buffer, action);
|
|
779
831
|
let token;
|
|
780
832
|
try {
|
|
781
833
|
token = await resolveAuthToken({ registry: normalizedRegistry, cwd, exec, env: authEnv });
|
|
@@ -2393,7 +2393,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2393
2393
|
|
|
2394
2394
|
// A non-automatable platform (kimi) has NO scriptable install CLI.
|
|
2395
2395
|
// It is handled entirely by its manual-requirement strategy, which
|
|
2396
|
-
// uses a stable
|
|
2396
|
+
// uses a stable plugin-level authority directory and deliberately SKIPS the
|
|
2397
2397
|
// per-run isolated consumer dir and its runDir containment check
|
|
2398
2398
|
// (that model only fits structured-cli platforms, which exec a real
|
|
2399
2399
|
// CLI into a per-run HOME).
|
|
@@ -2913,25 +2913,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2913
2913
|
});
|
|
2914
2914
|
}
|
|
2915
2915
|
|
|
2916
|
-
// installPath
|
|
2916
|
+
// installPath 验证:必须是真实目录,不得是符号链接。
|
|
2917
|
+
// 安装路径不要求位于 attestationDir 内(用户使用实际全局/用户安装目录)。
|
|
2917
2918
|
let verifiedInstallPath = null;
|
|
2918
2919
|
if (normalizedAttestation.installPath) {
|
|
2919
|
-
const managedRoot = resolve(attestationDir, 'kimi-home', 'plugins', 'managed');
|
|
2920
2920
|
const installPathAbs = resolve(normalizedAttestation.installPath);
|
|
2921
2921
|
|
|
2922
|
-
// 先验证 managed root 存在(KIMI_CODE_HOME 检查)
|
|
2923
|
-
const managedRootReal = await realpath(managedRoot).catch(() => null);
|
|
2924
|
-
if (!managedRootReal) {
|
|
2925
|
-
return createResult({
|
|
2926
|
-
actionType,
|
|
2927
|
-
status: ActionStatus.OBSERVED,
|
|
2928
|
-
observation: {
|
|
2929
|
-
installed: false,
|
|
2930
|
-
error: 'KIMI_CODE_HOME does not exist',
|
|
2931
|
-
},
|
|
2932
|
-
});
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
2922
|
// 检查 installPath 词法路径是否存在(lstat 在 realpath 之前)
|
|
2936
2923
|
let lexicalStat;
|
|
2937
2924
|
try {
|
|
@@ -2968,18 +2955,6 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2968
2955
|
},
|
|
2969
2956
|
});
|
|
2970
2957
|
}
|
|
2971
|
-
const rel = relative(managedRootReal, installPathReal);
|
|
2972
|
-
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
2973
|
-
if (rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)) {
|
|
2974
|
-
return createResult({
|
|
2975
|
-
actionType,
|
|
2976
|
-
status: ActionStatus.OBSERVED,
|
|
2977
|
-
observation: {
|
|
2978
|
-
installed: false,
|
|
2979
|
-
error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`,
|
|
2980
|
-
},
|
|
2981
|
-
});
|
|
2982
|
-
}
|
|
2983
2958
|
verifiedInstallPath = installPathReal;
|
|
2984
2959
|
}
|
|
2985
2960
|
|
|
@@ -3283,7 +3258,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
3283
3258
|
try {
|
|
3284
3259
|
codexRequirement = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_REQUIREMENT_FILE), 'utf8'));
|
|
3285
3260
|
} catch {
|
|
3286
|
-
// No requirement file — must not read
|
|
3261
|
+
// No requirement file — must not read the stable attestation
|
|
3287
3262
|
}
|
|
3288
3263
|
|
|
3289
3264
|
// Validate requirement binds to this action and plan (完整绑定验证)
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { loadProjectConfig } from '../core/config.mjs';
|
|
|
29
29
|
import { captureBaseline } from '../core/baseline.mjs';
|
|
30
30
|
import { runHook } from '../core/hooks.mjs';
|
|
31
31
|
import { computeHookCacheKey, readHookCache, writeHookCache } from '../core/hook-cache.mjs';
|
|
32
|
+
import { assertExpectedPublicSurface } from '../core/public-surface.mjs';
|
|
32
33
|
import { runSnapshotVerificationGates } from '../core/verification-gates.mjs';
|
|
33
34
|
import { createEvidenceWriter } from '../core/evidence.mjs';
|
|
34
35
|
import { computePlanDigest, writePlanAtomic, writePlanImmutable } from '../core/plan.mjs';
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
} from '../core/source-authority.mjs';
|
|
59
60
|
import { acquireProjectLock } from '../artifacts/project-lock.mjs';
|
|
60
61
|
import { assertPreviousPublicBaselineTarget, observePreviousPublicBaseline } from '../core/previous-public-baseline.mjs';
|
|
61
|
-
import {
|
|
62
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
62
63
|
import { createProductionPrepareRunDir } from '../core/run.mjs';
|
|
63
64
|
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
64
65
|
import { validateMarketplaceSourceSelection, MARKETPLACE_SOURCE_TYPES, resolvePluginManifestFromMarketplaceEntrySource, resolveMarketplaceRoot } from '../adapters/plugin-marketplace.mjs';
|
|
@@ -930,13 +931,13 @@ async function buildProductionAssets(
|
|
|
930
931
|
tarballDir: resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id),
|
|
931
932
|
expectedSnapshotDigest: sealed.digest,
|
|
932
933
|
});
|
|
933
|
-
await
|
|
934
|
+
await verifyFrozenNpmTarballContract({
|
|
934
935
|
package: npmDistribution.package,
|
|
935
936
|
version,
|
|
936
937
|
tarballPath: relative(root, npm.tarballPath),
|
|
937
938
|
tarballSha256: npm.sha256,
|
|
938
939
|
integrity: npm.integrity,
|
|
939
|
-
}, root);
|
|
940
|
+
}, root, resolveUnitScopedPath(resolve(runDir, 'tarballs'), unit.id));
|
|
940
941
|
}
|
|
941
942
|
|
|
942
943
|
assets.push({
|
|
@@ -1925,6 +1926,51 @@ export async function prepareRelease(options) {
|
|
|
1925
1926
|
});
|
|
1926
1927
|
}
|
|
1927
1928
|
|
|
1929
|
+
// --- Step 3b.1: Validate the post-build expected public surface ---
|
|
1930
|
+
// This gate runs after every declared hook (and the post-hook docs check)
|
|
1931
|
+
// but before source-authority closure, baseline, snapshots, remote reads,
|
|
1932
|
+
// and plan write. Therefore build-generated files must be explicitly
|
|
1933
|
+
// classified and every included source must already exist in publicFiles
|
|
1934
|
+
// before downstream authorities bind the release inputs.
|
|
1935
|
+
for (const unit of configUnits) {
|
|
1936
|
+
if (!unit?.expectedPublicSurface) continue;
|
|
1937
|
+
await evidence.append({
|
|
1938
|
+
phase: 'public-surface',
|
|
1939
|
+
status: 'started',
|
|
1940
|
+
unitId: unit.id,
|
|
1941
|
+
});
|
|
1942
|
+
try {
|
|
1943
|
+
const surface = await assertExpectedPublicSurface({
|
|
1944
|
+
root: realRoot,
|
|
1945
|
+
unit,
|
|
1946
|
+
});
|
|
1947
|
+
await evidence.append({
|
|
1948
|
+
phase: 'public-surface',
|
|
1949
|
+
status: 'completed',
|
|
1950
|
+
unitId: unit.id,
|
|
1951
|
+
...surface.summary,
|
|
1952
|
+
});
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
await evidence.append({
|
|
1955
|
+
phase: 'public-surface',
|
|
1956
|
+
status: 'failed',
|
|
1957
|
+
unitId: unit.id,
|
|
1958
|
+
reason: error.details?.reason ?? 'PUBLIC_SURFACE_CHECK_FAILED',
|
|
1959
|
+
error: {
|
|
1960
|
+
code: error.code,
|
|
1961
|
+
message: error.message,
|
|
1962
|
+
},
|
|
1963
|
+
diagnostics: {
|
|
1964
|
+
missingMappings: error.details?.missingMappings ?? [],
|
|
1965
|
+
unexpectedMappings: error.details?.unexpectedMappings ?? [],
|
|
1966
|
+
unclassifiedFiles: error.details?.unclassifiedFiles ?? [],
|
|
1967
|
+
ambiguousFiles: error.details?.ambiguousFiles ?? [],
|
|
1968
|
+
},
|
|
1969
|
+
});
|
|
1970
|
+
throw error;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1928
1974
|
// --- Step 3c: Source authority content closure gate ---
|
|
1929
1975
|
// After hooks complete, compute the deterministic source-input closure
|
|
1930
1976
|
// and verify that closure inputs are clean (no staged/unstaged/untracked
|
package/src/commands/publish.mjs
CHANGED
|
@@ -77,7 +77,7 @@ import {
|
|
|
77
77
|
verifyFrozenGitRepository,
|
|
78
78
|
verifyFrozenSnapshot,
|
|
79
79
|
} from '../snapshot/frozen.mjs';
|
|
80
|
-
import {
|
|
80
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
81
81
|
|
|
82
82
|
function assertInsideAssetRoot(assetRoot, candidate, label) {
|
|
83
83
|
const rel = relative(assetRoot, candidate);
|
|
@@ -607,7 +607,7 @@ export async function publishRelease(options) {
|
|
|
607
607
|
if (!npmDistribution) {
|
|
608
608
|
throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" has a frozen npm tarball but no npm distribution`);
|
|
609
609
|
}
|
|
610
|
-
await
|
|
610
|
+
await verifyFrozenNpmTarballContract({
|
|
611
611
|
package: npmDistribution.package,
|
|
612
612
|
version: unit.targetVersion,
|
|
613
613
|
tarballPath: frozen.npm.tarballPath,
|
|
@@ -62,6 +62,7 @@ import { assertTransition, PARTIAL, PUBLISHED, BLOCKED } from '../core/state-mac
|
|
|
62
62
|
import { verifySourceAuthorityReceipt } from '../core/source-authority.mjs';
|
|
63
63
|
import { matchObservation } from '../adapters/contract.mjs';
|
|
64
64
|
import { observeWithRetry, clampPolicyToTimeout, DEFAULT_OBSERVE_RETRY_POLICY } from '../core/observe-retry.mjs';
|
|
65
|
+
import { verifyFrozenNpmTarballContract } from '../adapters/npm.mjs';
|
|
65
66
|
|
|
66
67
|
// ---------------------------------------------------------------------------
|
|
67
68
|
// Constants
|
|
@@ -210,6 +211,44 @@ export async function reconcileRelease(options) {
|
|
|
210
211
|
|
|
211
212
|
await evidence.append({ phase: 'safety-gate', gate: 'action-completeness', status: 'passed' });
|
|
212
213
|
|
|
214
|
+
// =======================================================================
|
|
215
|
+
// Safety Gate 2c: Unconditional frozen npm entry-closure recheck
|
|
216
|
+
// This is plan-global and intentionally precedes source-run observation:
|
|
217
|
+
// a previously consistent npm checkpoint must not exempt a bad frozen
|
|
218
|
+
// tarball while another action is being reconciled.
|
|
219
|
+
// =======================================================================
|
|
220
|
+
for (const unit of plan.units ?? []) {
|
|
221
|
+
const frozen = unit.frozenSnapshot;
|
|
222
|
+
if (!frozen?.npm) continue;
|
|
223
|
+
const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === 'npm');
|
|
224
|
+
if (!npmDistribution) {
|
|
225
|
+
throw new ReleaseError(
|
|
226
|
+
GATE_FAILED,
|
|
227
|
+
`unit "${unit.id}" has a frozen npm tarball but no npm distribution`,
|
|
228
|
+
{ gate: 'npm-entry-closure', unitId: unit.id },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
await evidence.append({
|
|
232
|
+
phase: 'safety-gate',
|
|
233
|
+
gate: 'npm-entry-closure',
|
|
234
|
+
unitId: unit.id,
|
|
235
|
+
status: 'started',
|
|
236
|
+
});
|
|
237
|
+
await verifyFrozenNpmTarballContract({
|
|
238
|
+
package: npmDistribution.package,
|
|
239
|
+
version: unit.targetVersion,
|
|
240
|
+
tarballPath: frozen.npm.tarballPath,
|
|
241
|
+
tarballSha256: frozen.npm.tarballSha256,
|
|
242
|
+
integrity: frozen.npm.integrity,
|
|
243
|
+
}, root);
|
|
244
|
+
await evidence.append({
|
|
245
|
+
phase: 'safety-gate',
|
|
246
|
+
gate: 'npm-entry-closure',
|
|
247
|
+
unitId: unit.id,
|
|
248
|
+
status: 'passed',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
213
252
|
// =======================================================================
|
|
214
253
|
// Safety Gate 3: Load and validate source run
|
|
215
254
|
// =======================================================================
|