release-skill 0.2.3 → 0.2.5
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 +46 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +266 -913
- package/README.zh-CN.md +224 -535
- 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 +19805 -17556
- package/adapters/claude/schemas/release-plan.schema.json +228 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +155 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/codex/schemas/release-plan.schema.json +228 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +155 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/kimi/schemas/release-plan.schema.json +228 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +155 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
- package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
- package/bin/release-skill.bundle.mjs +19805 -17556
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +228 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +155 -2
- package/scripts/sync-public-files.mjs +20 -9
- package/src/adapters/plugin-marketplace.mjs +1237 -403
- package/src/commands/prepare.mjs +454 -40
- package/src/commands/publish.mjs +169 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +454 -20
- package/src/core/baseline.mjs +8 -1
- package/src/core/checkpoints.mjs +50 -7
- package/src/core/config.mjs +15 -0
- package/src/core/errors.mjs +2 -0
- package/src/core/installation-contract.mjs +341 -0
- package/src/core/plan.mjs +158 -6
- package/src/core/skill-resource-closure.mjs +425 -0
- package/src/platforms/codebuddy.mjs +206 -280
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +191 -119
- package/src/platforms/registry.mjs +24 -6
package/src/core/checkpoints.mjs
CHANGED
|
@@ -66,13 +66,14 @@ export const ADAPTER_ACTION_TYPE_MAP = {
|
|
|
66
66
|
* `push-snapshot` (the frozen commit must exist on the remote before a tag
|
|
67
67
|
* or branch tip can point at it). `npm-publish` has no git dependency and is
|
|
68
68
|
* placed in Tier 1 only for conservative scheduling.
|
|
69
|
-
* - Tier 2 `github-release`
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
69
|
+
* - Tier 2 `github-release` depends on Tier 1 `create-tag` (release `--verify-tag`).
|
|
70
|
+
*
|
|
71
|
+
* Marketplace actions (claude/codex/kimi/codebuddy-marketplace-install) are
|
|
72
|
+
* included in the tier table for ADAPTER_ACTION_TYPE_MAP lookup but are
|
|
73
|
+
* filtered out before tier grouping in both publish and reconcile. They are
|
|
74
|
+
* recorded as DEFERRED with CONSUMER_VERIFICATION_DEFERRED reason and never
|
|
75
|
+
* participate in tier execution. Their verification is handled exclusively
|
|
76
|
+
* by the verify command.
|
|
76
77
|
*
|
|
77
78
|
* Action types not listed in any tier are unknown to the scheduler and fail
|
|
78
79
|
* closed (see groupActionsByTier); they are never silently scheduled.
|
|
@@ -146,3 +147,45 @@ export function groupActionsByTier(orderedActions) {
|
|
|
146
147
|
}
|
|
147
148
|
return { tiers, unknown };
|
|
148
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 远端写入动作类型集合。
|
|
153
|
+
* 这些动作的结果决定 PUBLISHED 状态:全部一致后即可进入 PUBLISHED。
|
|
154
|
+
*/
|
|
155
|
+
export const REMOTE_WRITE_ACTION_TYPES = new Set([
|
|
156
|
+
'push-commit',
|
|
157
|
+
'push-snapshot',
|
|
158
|
+
'set-default-branch',
|
|
159
|
+
'create-tag',
|
|
160
|
+
'npm-publish',
|
|
161
|
+
'github-release',
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 市场安装动作类型集合。
|
|
166
|
+
* 这些动作的结果记录在 run 中,但不阻止 PUBLISHED 状态。
|
|
167
|
+
*/
|
|
168
|
+
export const MARKETPLACE_ACTION_TYPES = new Set([
|
|
169
|
+
'claude-marketplace-install',
|
|
170
|
+
'codex-marketplace-install',
|
|
171
|
+
'kimi-marketplace-install',
|
|
172
|
+
'codebuddy-marketplace-install',
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 判断动作类型是否为远端写入动作。
|
|
177
|
+
* @param {string} actionType - 计划中的动作类型
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
export function isRemoteWriteAction(actionType) {
|
|
181
|
+
return REMOTE_WRITE_ACTION_TYPES.has(actionType);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 判断动作类型是否为市场安装动作。
|
|
186
|
+
* @param {string} actionType - 计划中的动作类型
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
export function isMarketplaceAction(actionType) {
|
|
190
|
+
return MARKETPLACE_ACTION_TYPES.has(actionType);
|
|
191
|
+
}
|
package/src/core/config.mjs
CHANGED
|
@@ -346,6 +346,21 @@ export async function loadProjectConfig({ root, configPath } = {}) {
|
|
|
346
346
|
}
|
|
347
347
|
} // end of contextual prevalidation else block
|
|
348
348
|
|
|
349
|
+
// --- Normalize marketplaceSourceType for old configs ---
|
|
350
|
+
// Old configs may lack marketplaceSourceType; determine by marketplaceRepo existence.
|
|
351
|
+
// This runs before schema validation so the required rule passes.
|
|
352
|
+
if (Array.isArray(config.releaseUnits)) {
|
|
353
|
+
for (const unit of config.releaseUnits) {
|
|
354
|
+
if (!unit?.distributions) continue;
|
|
355
|
+
for (const dist of unit.distributions) {
|
|
356
|
+
if (dist.type === 'npm') continue;
|
|
357
|
+
if (dist.marketplaceSourceType === undefined || dist.marketplaceSourceType === null) {
|
|
358
|
+
dist.marketplaceSourceType = dist.marketplaceRepo ? 'standalone-index' : 'bundled-family';
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
349
364
|
// --- Schema validation (using formal JSON schema) ---
|
|
350
365
|
const valid = validateConfig(config);
|
|
351
366
|
if (!valid) {
|
package/src/core/errors.mjs
CHANGED
|
@@ -87,6 +87,7 @@ const EXIT_CODE_MAP = Object.freeze({
|
|
|
87
87
|
RELEASE_DOCS_CONFLICT: 44,
|
|
88
88
|
RELEASE_DOCS_REFRESH_STALE: 45,
|
|
89
89
|
RELEASE_DOCS_STALE: 46,
|
|
90
|
+
CONSUMER_VERIFICATION_DEFERRED: 47,
|
|
90
91
|
});
|
|
91
92
|
|
|
92
93
|
// ---- Error code constants ----
|
|
@@ -128,6 +129,7 @@ export const RELEASE_DOCS_TRANSLATION_MISSING = 'RELEASE_DOCS_TRANSLATION_MISSIN
|
|
|
128
129
|
export const RELEASE_DOCS_CONFLICT = 'RELEASE_DOCS_CONFLICT';
|
|
129
130
|
export const RELEASE_DOCS_REFRESH_STALE = 'RELEASE_DOCS_REFRESH_STALE';
|
|
130
131
|
export const RELEASE_DOCS_STALE = 'RELEASE_DOCS_STALE';
|
|
132
|
+
export const CONSUMER_VERIFICATION_DEFERRED = 'CONSUMER_VERIFICATION_DEFERRED';
|
|
131
133
|
|
|
132
134
|
/**
|
|
133
135
|
* Typed error for release-skill operations.
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安装契约(Installation Contract)。
|
|
3
|
+
*
|
|
4
|
+
* 为每个平台构建一个可审计的规范化安装契约对象,并基于其规范 JSON 计算 SHA-256 摘要。
|
|
5
|
+
* 用于在远端状态未变化时跳过重复验证(NOT_REQUIRED_UNCHANGED)。
|
|
6
|
+
*
|
|
7
|
+
* 契约对象包含:
|
|
8
|
+
* - 算法版本
|
|
9
|
+
* - distributionType
|
|
10
|
+
* - 插件 manifest 的实际相对路径
|
|
11
|
+
* - 规范化后的 manifest 安装内容
|
|
12
|
+
* - marketplaceSourceType
|
|
13
|
+
* - 是否纳入市场条目
|
|
14
|
+
* - 若纳入:所选市场索引的相对路径/来源标识、规范化后的唯一插件条目
|
|
15
|
+
* - verificationRecipeVersion
|
|
16
|
+
*
|
|
17
|
+
* 不纳入契约的字段(做静态一致性校验):
|
|
18
|
+
* - 版本号(version)
|
|
19
|
+
* - 描述(description / shortDescription / longDescription)
|
|
20
|
+
* - 默认提示词(defaultPrompt)
|
|
21
|
+
* - 标签(tag)
|
|
22
|
+
* - 提交信息(commit / commitSha / marketplaceCommitSha / sha)
|
|
23
|
+
*
|
|
24
|
+
* @module core/installation-contract
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 安装契约摘要算法版本。
|
|
31
|
+
* 算法变更时递增;版本升级会强制重新验证。
|
|
32
|
+
*/
|
|
33
|
+
export const INSTALLATION_CONTRACT_ALGORITHM_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 合法的 marketplaceSourceType 值。
|
|
37
|
+
*/
|
|
38
|
+
const VALID_MARKETPLACE_SOURCE_TYPES = new Set([
|
|
39
|
+
'bundled-family',
|
|
40
|
+
'standalone-index',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 合法的消费端验证结果类型(用于免验判定)。
|
|
45
|
+
*/
|
|
46
|
+
const VALID_CONSUMER_VERIFICATION_STATUSES = new Set([
|
|
47
|
+
'PASSED_AUTOMATIC',
|
|
48
|
+
'PASSED_MANUAL',
|
|
49
|
+
'NOT_REQUIRED_UNCHANGED',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 需要从规范化对象中递归删除的展示/发布身份校验字段。
|
|
54
|
+
*/
|
|
55
|
+
const DISPLAY_FIELDS_TO_STRIP = new Set([
|
|
56
|
+
'version',
|
|
57
|
+
'description',
|
|
58
|
+
'shortDescription',
|
|
59
|
+
'longDescription',
|
|
60
|
+
'defaultPrompt',
|
|
61
|
+
'tag',
|
|
62
|
+
'commit',
|
|
63
|
+
'commitSha',
|
|
64
|
+
'marketplaceCommitSha',
|
|
65
|
+
'sha',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 递归删除对象中的展示字段(深度克隆)。
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} obj - 源对象
|
|
72
|
+
* @returns {Object} 规范化后的对象
|
|
73
|
+
*/
|
|
74
|
+
function stripDisplayFields(obj) {
|
|
75
|
+
if (obj === null || typeof obj !== 'object') {
|
|
76
|
+
return obj;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(obj)) {
|
|
80
|
+
return obj.map(item => stripDisplayFields(item));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const key of Object.keys(obj)) {
|
|
85
|
+
if (DISPLAY_FIELDS_TO_STRIP.has(key)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
result[key] = stripDisplayFields(obj[key]);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 深度冻结对象(递归)。
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} obj - 要冻结的对象
|
|
97
|
+
* @returns {Object} 冻结后的对象
|
|
98
|
+
*/
|
|
99
|
+
function deepFreeze(obj) {
|
|
100
|
+
if (obj === null || typeof obj !== 'object') {
|
|
101
|
+
return obj;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Object.freeze(obj);
|
|
105
|
+
|
|
106
|
+
if (Array.isArray(obj)) {
|
|
107
|
+
for (const item of obj) {
|
|
108
|
+
deepFreeze(item);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
for (const value of Object.values(obj)) {
|
|
112
|
+
deepFreeze(value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 验证路径是否为相对路径。
|
|
121
|
+
*
|
|
122
|
+
* @param {string} path - 路径
|
|
123
|
+
* @param {string} fieldName - 字段名(用于错误信息)
|
|
124
|
+
*/
|
|
125
|
+
function assertRelativePath(path, fieldName) {
|
|
126
|
+
if (typeof path !== 'string') {
|
|
127
|
+
throw new Error(`${fieldName} must be a string`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (path === '') {
|
|
131
|
+
throw new Error(`${fieldName} must be a non-empty string`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Unix 绝对路径
|
|
135
|
+
if (path.startsWith('/')) {
|
|
136
|
+
throw new Error(`${fieldName} must be a relative path, got absolute path: ${path}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Windows 绝对路径
|
|
140
|
+
if (/^[a-zA-Z]:\\/.test(path) || /^[a-zA-Z]:\//.test(path)) {
|
|
141
|
+
throw new Error(`${fieldName} must be a relative path, got absolute path: ${path}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// UNC 路径
|
|
145
|
+
if (path.startsWith('\\\\')) {
|
|
146
|
+
throw new Error(`${fieldName} must be a relative path, got UNC path: ${path}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 目录穿越检查
|
|
150
|
+
const segments = path.split('/');
|
|
151
|
+
for (const seg of segments) {
|
|
152
|
+
if (seg === '..') {
|
|
153
|
+
throw new Error(`${fieldName} must not contain ".." traversal, got: ${path}`);
|
|
154
|
+
}
|
|
155
|
+
if (seg === '.') {
|
|
156
|
+
throw new Error(`${fieldName} must not contain "." component, got: ${path}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 验证摘要格式是否为合法的 64 位十六进制字符串。
|
|
163
|
+
*
|
|
164
|
+
* @param {string} digest - 摘要
|
|
165
|
+
* @returns {boolean} 是否合法
|
|
166
|
+
*/
|
|
167
|
+
function isValidDigest(digest) {
|
|
168
|
+
return typeof digest === 'string' && /^[a-f0-9]{64}$/.test(digest);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 构建可审计的安装契约对象。
|
|
173
|
+
*
|
|
174
|
+
* @param {Object} params
|
|
175
|
+
* @param {string} params.distributionType - 分发类型(如 'claude-plugin', 'kimi-plugin')
|
|
176
|
+
* @param {string} params.manifestRelativePath - 插件 manifest 的相对路径
|
|
177
|
+
* @param {Object} params.manifest - 插件 manifest 对象
|
|
178
|
+
* @param {string} params.marketplaceSourceType - 市场来源类型
|
|
179
|
+
* @param {boolean} params.includeMarketplaceEntry - 是否纳入市场条目
|
|
180
|
+
* @param {string} [params.marketplaceIndexRelativePath] - 市场索引相对路径(includeMarketplaceEntry=true 时必需)
|
|
181
|
+
* @param {Object} [params.selectedMarketplaceEntry] - 所选市场条目(includeMarketplaceEntry=true 时必需)
|
|
182
|
+
* @param {string} params.verificationRecipeVersion - 验证配方版本
|
|
183
|
+
* @returns {Object} 深度冻结的契约对象
|
|
184
|
+
*/
|
|
185
|
+
export function buildInstallationContract({
|
|
186
|
+
distributionType,
|
|
187
|
+
manifestRelativePath,
|
|
188
|
+
manifest,
|
|
189
|
+
marketplaceSourceType,
|
|
190
|
+
includeMarketplaceEntry,
|
|
191
|
+
marketplaceIndexRelativePath,
|
|
192
|
+
selectedMarketplaceEntry,
|
|
193
|
+
verificationRecipeVersion,
|
|
194
|
+
} = {}) {
|
|
195
|
+
// 输入验证
|
|
196
|
+
if (!distributionType || typeof distributionType !== 'string') {
|
|
197
|
+
throw new Error('buildInstallationContract: distributionType must be a non-empty string');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!manifestRelativePath || typeof manifestRelativePath !== 'string') {
|
|
201
|
+
throw new Error('buildInstallationContract: manifestRelativePath must be a non-empty string');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
assertRelativePath(manifestRelativePath, 'manifestRelativePath');
|
|
205
|
+
|
|
206
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
207
|
+
throw new Error('buildInstallationContract: manifest must be a non-null plain object');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!marketplaceSourceType || typeof marketplaceSourceType !== 'string') {
|
|
211
|
+
throw new Error('buildInstallationContract: marketplaceSourceType must be a non-empty string');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!VALID_MARKETPLACE_SOURCE_TYPES.has(marketplaceSourceType)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`buildInstallationContract: invalid marketplaceSourceType "${marketplaceSourceType}", ` +
|
|
217
|
+
`must be one of: ${[...VALID_MARKETPLACE_SOURCE_TYPES].join(', ')}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!verificationRecipeVersion || typeof verificationRecipeVersion !== 'string') {
|
|
222
|
+
throw new Error('buildInstallationContract: verificationRecipeVersion must be a non-empty string');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof includeMarketplaceEntry !== 'boolean') {
|
|
226
|
+
throw new Error('buildInstallationContract: includeMarketplaceEntry must be a boolean');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 市场条目验证
|
|
230
|
+
if (includeMarketplaceEntry) {
|
|
231
|
+
if (!selectedMarketplaceEntry || typeof selectedMarketplaceEntry !== 'object' || Array.isArray(selectedMarketplaceEntry)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
'buildInstallationContract: selectedMarketplaceEntry is required when includeMarketplaceEntry is true'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!marketplaceIndexRelativePath || typeof marketplaceIndexRelativePath !== 'string') {
|
|
238
|
+
throw new Error(
|
|
239
|
+
'buildInstallationContract: marketplaceIndexRelativePath is required when includeMarketplaceEntry is true'
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
assertRelativePath(marketplaceIndexRelativePath, 'marketplaceIndexRelativePath');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 构建契约对象
|
|
247
|
+
const contract = {
|
|
248
|
+
algorithmVersion: INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
249
|
+
distributionType,
|
|
250
|
+
manifestRelativePath,
|
|
251
|
+
normalizedManifest: stripDisplayFields(manifest),
|
|
252
|
+
marketplaceSourceType,
|
|
253
|
+
includeMarketplaceEntry,
|
|
254
|
+
verificationRecipeVersion,
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// 纳入市场条目
|
|
258
|
+
if (includeMarketplaceEntry) {
|
|
259
|
+
contract.marketplaceIndexRelativePath = marketplaceIndexRelativePath;
|
|
260
|
+
contract.normalizedSelectedEntry = stripDisplayFields(selectedMarketplaceEntry);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 深度冻结并返回
|
|
264
|
+
return deepFreeze(contract);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 计算安装契约摘要。
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} params - buildInstallationContract 的参数
|
|
271
|
+
* @returns {string} SHA-256 摘要(64 位十六进制)
|
|
272
|
+
*/
|
|
273
|
+
export function computeInstallationContractDigest(params) {
|
|
274
|
+
const contract = buildInstallationContract(params);
|
|
275
|
+
return sha256Hex(canonicalJson(contract));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 判断是否可以跳过验证。
|
|
280
|
+
*
|
|
281
|
+
* 以下条件同时成立才返回 NOT_REQUIRED_UNCHANGED:
|
|
282
|
+
* 1. 当前摘要是合法 64 位十六进制
|
|
283
|
+
* 2. 上次摘要相同
|
|
284
|
+
* 3. 上次结果明确是消费端验证已解决类型(PASSED_AUTOMATIC / PASSED_MANUAL / NOT_REQUIRED_UNCHANGED)
|
|
285
|
+
* 4. 上次收据中记录的算法版本与当前算法版本相同
|
|
286
|
+
* 5. 上次收据自身绑定相同的 installationContractDigest
|
|
287
|
+
*
|
|
288
|
+
* @param {Object} params
|
|
289
|
+
* @param {string} params.currentDigest - 当前计算的安装契约摘要
|
|
290
|
+
* @param {string|null} params.previousDigest - 上一成功验证的摘要(可为 null)
|
|
291
|
+
* @param {Object|null} params.previousReceipt - 上一成功验证的收据(可为 null)
|
|
292
|
+
* @param {number} params.algorithmVersion - 当前算法版本
|
|
293
|
+
* @returns {'NOT_REQUIRED_UNCHANGED' | 'REQUIRE_VERIFICATION'}
|
|
294
|
+
*/
|
|
295
|
+
export function shouldSkipVerification({
|
|
296
|
+
currentDigest,
|
|
297
|
+
previousDigest,
|
|
298
|
+
previousReceipt,
|
|
299
|
+
algorithmVersion,
|
|
300
|
+
} = {}) {
|
|
301
|
+
// 条件 1:当前摘要必须合法
|
|
302
|
+
if (!isValidDigest(currentDigest)) {
|
|
303
|
+
return 'REQUIRE_VERIFICATION';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 条件 2:前次摘要必须存在且相同
|
|
307
|
+
if (!previousDigest || currentDigest !== previousDigest) {
|
|
308
|
+
return 'REQUIRE_VERIFICATION';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 条件 3:前次收据必须存在
|
|
312
|
+
if (!previousReceipt || typeof previousReceipt !== 'object') {
|
|
313
|
+
return 'REQUIRE_VERIFICATION';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 条件 4:前次收据结果必须是合法的消费端验证结果
|
|
317
|
+
// 只认 result 字段(schema 定义的正式字段);新消费端收据此前不存在正式 status,
|
|
318
|
+
// 仅有 status 无 result 时要求重新验证。
|
|
319
|
+
if (!VALID_CONSUMER_VERIFICATION_STATUSES.has(previousReceipt.result)) {
|
|
320
|
+
return 'REQUIRE_VERIFICATION';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 条件 5:算法版本必须一致
|
|
324
|
+
const previousAlgorithmVersion = previousReceipt.algorithmVersion
|
|
325
|
+
?? previousReceipt.installationContractAlgorithmVersion;
|
|
326
|
+
if (previousAlgorithmVersion !== algorithmVersion) {
|
|
327
|
+
return 'REQUIRE_VERIFICATION';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 条件 6:收据自身绑定的摘要必须一致
|
|
331
|
+
if (!isValidDigest(previousReceipt.installationContractDigest)) {
|
|
332
|
+
return 'REQUIRE_VERIFICATION';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (previousReceipt.installationContractDigest !== currentDigest) {
|
|
336
|
+
return 'REQUIRE_VERIFICATION';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 全部条件满足 => 跳过验证
|
|
340
|
+
return 'NOT_REQUIRED_UNCHANGED';
|
|
341
|
+
}
|
package/src/core/plan.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import addFormats from 'ajv-formats';
|
|
|
22
22
|
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
23
23
|
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
24
24
|
import { readTrustedPackageResource } from './trusted-resource.mjs';
|
|
25
|
+
import { computeInstallationContractDigest, INSTALLATION_CONTRACT_ALGORITHM_VERSION } from './installation-contract.mjs';
|
|
25
26
|
// NOTE (T2.2 step 3): plan.mjs and the platform registry form a module cycle
|
|
26
27
|
// (plan -> registry -> platforms/kimi -> plan, because the kimi strategies
|
|
27
28
|
// bind computePlanDigest). PLATFORMS is therefore only ever read inside
|
|
@@ -164,6 +165,35 @@ export function validatePlan(plan) {
|
|
|
164
165
|
);
|
|
165
166
|
}
|
|
166
167
|
}
|
|
168
|
+
|
|
169
|
+
if (plan.skillResourceClosure) {
|
|
170
|
+
const receipts = plan.skillResourceClosure.unitReceipts ?? [];
|
|
171
|
+
const receiptUnitIds = receipts.map((item) => item.unitId);
|
|
172
|
+
if (
|
|
173
|
+
new Set(receiptUnitIds).size !== receiptUnitIds.length
|
|
174
|
+
|| JSON.stringify([...receiptUnitIds].sort()) !== JSON.stringify([...unitsById.keys()].sort())
|
|
175
|
+
) {
|
|
176
|
+
throw new ReleaseError(
|
|
177
|
+
GATE_FAILED,
|
|
178
|
+
'skill resource closure receipts must cover every release unit exactly once',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const totals = {
|
|
182
|
+
totalSkillCount: receipts.reduce((sum, item) => sum + item.skillCount, 0),
|
|
183
|
+
totalReferenceCount: receipts.reduce((sum, item) => sum + item.referenceCount, 0),
|
|
184
|
+
totalSourceOnlyCount: receipts.reduce((sum, item) => sum + item.sourceOnlyCount, 0),
|
|
185
|
+
totalFindingCount: receipts.reduce((sum, item) => sum + item.findingCount, 0),
|
|
186
|
+
};
|
|
187
|
+
for (const [field, expected] of Object.entries(totals)) {
|
|
188
|
+
if (plan.skillResourceClosure[field] !== expected) {
|
|
189
|
+
throw new ReleaseError(
|
|
190
|
+
GATE_FAILED,
|
|
191
|
+
`skill resource closure ${field} does not match unit receipts`,
|
|
192
|
+
{ expected, observed: plan.skillResourceClosure[field] },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
/**
|
|
@@ -778,17 +808,71 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
778
808
|
|
|
779
809
|
// External independent marketplace form: the distribution declares
|
|
780
810
|
// marketplaceRepo, so the install targets the external marketplace
|
|
781
|
-
// repository instead of publicRepo.
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
811
|
+
// repository instead of publicRepo. All platforms support both
|
|
812
|
+
// bundled-family and standalone-index source types; the marketplace
|
|
813
|
+
// add capability (marketplaceRefForm) only constrains the automated
|
|
814
|
+
// install path, not the static source validation.
|
|
785
815
|
const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
|
|
786
|
-
if (externalMarketplace &&
|
|
816
|
+
if (externalMarketplace && dist.marketplaceSourceType && dist.marketplaceSourceType !== 'standalone-index') {
|
|
787
817
|
failures.push(
|
|
788
|
-
`unit "${unitId}", action "${action.id}": ${platform.distributionType} distribution declares marketplaceRepo but
|
|
818
|
+
`unit "${unitId}", action "${action.id}": ${platform.distributionType} distribution declares marketplaceRepo but marketplaceSourceType is "${dist.marketplaceSourceType}"; marketplaceRepo requires standalone-index`,
|
|
789
819
|
);
|
|
790
820
|
}
|
|
791
821
|
|
|
822
|
+
// --- 新版字段组完整性检查 ---
|
|
823
|
+
// 真正新增的安装契约字段(0.2.3 旧计划不含这些字段):
|
|
824
|
+
// distribution: installationContract、installationContractDigest、marketplaceSourceType
|
|
825
|
+
// action: installationContractDigest、algorithmVersion
|
|
826
|
+
// 0.2.3 已有的 marketplaceForm/sourceDescriptor/sourceCommit 单独存在不能触发新版组,
|
|
827
|
+
// 否则破坏已发布 0.2.3 冻结计划兼容。
|
|
828
|
+
// 整组都不存在才按旧计划兼容;出现任意一个但不完整必须失败。
|
|
829
|
+
const distHasFieldGroup = dist.installationContract !== undefined && dist.installationContract !== null
|
|
830
|
+
|| dist.installationContractDigest !== undefined && dist.installationContractDigest !== null
|
|
831
|
+
|| dist.marketplaceSourceType !== undefined && dist.marketplaceSourceType !== null;
|
|
832
|
+
const actionHasFieldGroup = action.parameters?.installationContractDigest !== undefined && action.parameters?.installationContractDigest !== null
|
|
833
|
+
|| action.parameters?.algorithmVersion !== undefined && action.parameters?.algorithmVersion !== null;
|
|
834
|
+
const hasFieldGroup = distHasFieldGroup || actionHasFieldGroup;
|
|
835
|
+
|
|
836
|
+
if (hasFieldGroup) {
|
|
837
|
+
// 检查 distribution 字段完整性
|
|
838
|
+
if (dist.marketplaceSourceType === undefined || dist.marketplaceSourceType === null) {
|
|
839
|
+
failures.push(`unit "${unitId}", action "${action.id}": distribution missing marketplaceSourceType; new-version field group requires all fields`);
|
|
840
|
+
}
|
|
841
|
+
if (dist.installationContract === undefined || dist.installationContract === null) {
|
|
842
|
+
failures.push(`unit "${unitId}", action "${action.id}": distribution missing installationContract; new-version field group requires all fields`);
|
|
843
|
+
}
|
|
844
|
+
if (dist.installationContractDigest === undefined || dist.installationContractDigest === null) {
|
|
845
|
+
failures.push(`unit "${unitId}", action "${action.id}": distribution missing installationContractDigest; new-version field group requires all fields`);
|
|
846
|
+
}
|
|
847
|
+
// 检查 action 字段完整性
|
|
848
|
+
if (action.parameters?.marketplaceForm === undefined || action.parameters?.marketplaceForm === null) {
|
|
849
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.marketplaceForm missing; new-version field group requires all fields`);
|
|
850
|
+
}
|
|
851
|
+
if (action.parameters?.sourceDescriptor === undefined || action.parameters?.sourceDescriptor === null) {
|
|
852
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.sourceDescriptor missing; new-version field group requires all fields`);
|
|
853
|
+
}
|
|
854
|
+
if (action.parameters?.installationContractDigest === undefined || action.parameters?.installationContractDigest === null) {
|
|
855
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.installationContractDigest missing; new-version field group requires all fields`);
|
|
856
|
+
}
|
|
857
|
+
if (action.parameters?.algorithmVersion === undefined || action.parameters?.algorithmVersion === null) {
|
|
858
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.algorithmVersion missing; new-version field group requires all fields`);
|
|
859
|
+
}
|
|
860
|
+
// sourceCommit 仅生产计划必需
|
|
861
|
+
if (production && (action.parameters?.sourceCommit === undefined || action.parameters?.sourceCommit === null)) {
|
|
862
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.sourceCommit missing; production plan requires sourceCommit`);
|
|
863
|
+
}
|
|
864
|
+
// marketplaceForm、sourceDescriptor.form、distribution 的 marketplaceSourceType 三者一致
|
|
865
|
+
const mf = action.parameters?.marketplaceForm;
|
|
866
|
+
const sdForm = action.parameters?.sourceDescriptor?.form;
|
|
867
|
+
const distSourceType = dist.marketplaceSourceType;
|
|
868
|
+
if (mf && distSourceType && mf !== distSourceType) {
|
|
869
|
+
failures.push(`unit "${unitId}", action "${action.id}": marketplaceForm "${mf}" does not match distribution marketplaceSourceType "${distSourceType}"`);
|
|
870
|
+
}
|
|
871
|
+
if (sdForm && distSourceType && sdForm !== distSourceType) {
|
|
872
|
+
failures.push(`unit "${unitId}", action "${action.id}": sourceDescriptor.form "${sdForm}" does not match distribution marketplaceSourceType "${distSourceType}"`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
792
876
|
// Parameter checks
|
|
793
877
|
_checkRequired(action, 'parameters.consumer', action.parameters?.consumer, platform.id, unitId, failures);
|
|
794
878
|
_checkRequired(action, 'parameters.plugin', action.parameters?.plugin, plugin, unitId, failures);
|
|
@@ -1060,6 +1144,74 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
1060
1144
|
}
|
|
1061
1145
|
}
|
|
1062
1146
|
|
|
1147
|
+
// --- installationContractDigest 重新计算验证 ---
|
|
1148
|
+
// 不能只比较两个可一起篡改的字符串;必须重新由 distribution 的 installationContract 计算并验证
|
|
1149
|
+
if (hasFieldGroup && dist.installationContract) {
|
|
1150
|
+
// 从 distribution 的 installationContract 重新计算摘要
|
|
1151
|
+
const recomputedDigest = sha256Hex(canonicalJson(dist.installationContract));
|
|
1152
|
+
const actionDigest = action.parameters?.installationContractDigest;
|
|
1153
|
+
const distDigest = dist.installationContractDigest;
|
|
1154
|
+
|
|
1155
|
+
// 重算值必须同时等于 distribution 摘要和 action 摘要
|
|
1156
|
+
if (distDigest && recomputedDigest !== distDigest) {
|
|
1157
|
+
failures.push(
|
|
1158
|
+
`unit "${unitId}", action "${action.id}": recomputed installationContractDigest "${recomputedDigest.slice(0, 16)}..." does not match distribution digest "${(distDigest ?? '').slice(0, 16)}..."; contract may have been tampered`,
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
if (actionDigest && recomputedDigest !== actionDigest) {
|
|
1162
|
+
failures.push(
|
|
1163
|
+
`unit "${unitId}", action "${action.id}": recomputed installationContractDigest "${recomputedDigest.slice(0, 16)}..." does not match action digest "${(actionDigest ?? '').slice(0, 16)}..."; contract may have been tampered`,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
// algorithmVersion 必须等于当前算法版本
|
|
1167
|
+
if (action.parameters?.algorithmVersion !== undefined && action.parameters?.algorithmVersion !== INSTALLATION_CONTRACT_ALGORITHM_VERSION) {
|
|
1168
|
+
failures.push(
|
|
1169
|
+
`unit "${unitId}", action "${action.id}": algorithmVersion is "${action.parameters?.algorithmVersion}", expected "${INSTALLATION_CONTRACT_ALGORITHM_VERSION}"`,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// --- standalone-index 审计字段校验 ---
|
|
1175
|
+
// 仅 production === true 且来源为 standalone-index 时,要求 action 完整携带
|
|
1176
|
+
// 非空 marketplaceIndexPath / marketplaceName / selectedEntry。
|
|
1177
|
+
// 非生产计划不得要求这三个字段存在(未冻结时无真实值可用),
|
|
1178
|
+
// 也不得接受"用 null 表示存在"的方案。
|
|
1179
|
+
if (production && externalMarketplace && dist.marketplaceSourceType === 'standalone-index') {
|
|
1180
|
+
const params = action.parameters;
|
|
1181
|
+
if (params) {
|
|
1182
|
+
if (!params.marketplaceIndexPath) {
|
|
1183
|
+
failures.push(
|
|
1184
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceIndexPath is required for production standalone-index`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
if (!params.marketplaceName) {
|
|
1188
|
+
failures.push(
|
|
1189
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceName is required for production standalone-index`,
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (!params.selectedEntry) {
|
|
1193
|
+
failures.push(
|
|
1194
|
+
`unit "${unitId}", action "${action.id}": parameters.selectedEntry is required for production standalone-index`,
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// --- bundled-family 禁止混入 standalone 字段 ---
|
|
1201
|
+
if (!externalMarketplace && dist.marketplaceSourceType === 'bundled-family') {
|
|
1202
|
+
const params = action.parameters;
|
|
1203
|
+
if (params) {
|
|
1204
|
+
const STANDALONE_ONLY = ['marketplaceIndexPath', 'marketplaceName', 'selectedEntry'];
|
|
1205
|
+
for (const field of STANDALONE_ONLY) {
|
|
1206
|
+
if (params[field] !== undefined) {
|
|
1207
|
+
failures.push(
|
|
1208
|
+
`unit "${unitId}", action "${action.id}": parameters.${field} is unexpected for bundled-family form; mixed fields are prohibited`,
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1063
1215
|
// Expected checks. Marketplace identity is bound only on platforms
|
|
1064
1216
|
// whose schema requires it (MINOR-1): a non-marketplace platform's
|
|
1065
1217
|
// observation never binds a marketplace.
|