release-skill 0.2.2 → 0.2.4
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 +4 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +27 -0
- package/INSTALL.md +95 -139
- package/INSTALL.zh-CN.md +70 -121
- package/README.md +265 -916
- package/README.zh-CN.md +222 -537
- 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 +18710 -16694
- package/adapters/claude/schemas/release-plan.schema.json +137 -0
- package/adapters/claude/schemas/release-project.schema.json +93 -0
- package/adapters/claude/schemas/release-run.schema.json +70 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/codex/schemas/release-plan.schema.json +137 -0
- package/adapters/codex/schemas/release-project.schema.json +93 -0
- package/adapters/codex/schemas/release-run.schema.json +70 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/kimi/schemas/release-plan.schema.json +137 -0
- package/adapters/kimi/schemas/release-project.schema.json +93 -0
- package/adapters/kimi/schemas/release-run.schema.json +70 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +18710 -16694
- package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
- package/bin/release-skill.bundle.mjs +18710 -16694
- package/package.json +1 -1
- package/schemas/release-plan.schema.json +137 -0
- package/schemas/release-project.schema.json +93 -0
- package/schemas/release-run.schema.json +70 -2
- package/src/adapters/plugin-marketplace.mjs +1602 -605
- package/src/commands/prepare.mjs +441 -32
- package/src/commands/publish.mjs +107 -75
- package/src/commands/reconcile.mjs +92 -327
- package/src/commands/setup.mjs +148 -20
- package/src/commands/verify.mjs +315 -25
- package/src/core/baseline.mjs +21 -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 +307 -6
- package/src/platforms/codebuddy.mjs +191 -238
- package/src/platforms/codex.mjs +369 -0
- package/src/platforms/kimi.mjs +164 -119
- package/src/platforms/registry.mjs +180 -4
- package/src/producers/build-adapters.mjs +9 -2
|
@@ -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
|
|
@@ -778,17 +779,71 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
778
779
|
|
|
779
780
|
// External independent marketplace form: the distribution declares
|
|
780
781
|
// marketplaceRepo, so the install targets the external marketplace
|
|
781
|
-
// repository instead of publicRepo.
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
782
|
+
// repository instead of publicRepo. All platforms support both
|
|
783
|
+
// bundled-family and standalone-index source types; the marketplace
|
|
784
|
+
// add capability (marketplaceRefForm) only constrains the automated
|
|
785
|
+
// install path, not the static source validation.
|
|
785
786
|
const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
|
|
786
|
-
if (externalMarketplace &&
|
|
787
|
+
if (externalMarketplace && dist.marketplaceSourceType && dist.marketplaceSourceType !== 'standalone-index') {
|
|
787
788
|
failures.push(
|
|
788
|
-
`unit "${unitId}", action "${action.id}": ${platform.distributionType} distribution declares marketplaceRepo but
|
|
789
|
+
`unit "${unitId}", action "${action.id}": ${platform.distributionType} distribution declares marketplaceRepo but marketplaceSourceType is "${dist.marketplaceSourceType}"; marketplaceRepo requires standalone-index`,
|
|
789
790
|
);
|
|
790
791
|
}
|
|
791
792
|
|
|
793
|
+
// --- 新版字段组完整性检查 ---
|
|
794
|
+
// 真正新增的安装契约字段(0.2.3 旧计划不含这些字段):
|
|
795
|
+
// distribution: installationContract、installationContractDigest、marketplaceSourceType
|
|
796
|
+
// action: installationContractDigest、algorithmVersion
|
|
797
|
+
// 0.2.3 已有的 marketplaceForm/sourceDescriptor/sourceCommit 单独存在不能触发新版组,
|
|
798
|
+
// 否则破坏已发布 0.2.3 冻结计划兼容。
|
|
799
|
+
// 整组都不存在才按旧计划兼容;出现任意一个但不完整必须失败。
|
|
800
|
+
const distHasFieldGroup = dist.installationContract !== undefined && dist.installationContract !== null
|
|
801
|
+
|| dist.installationContractDigest !== undefined && dist.installationContractDigest !== null
|
|
802
|
+
|| dist.marketplaceSourceType !== undefined && dist.marketplaceSourceType !== null;
|
|
803
|
+
const actionHasFieldGroup = action.parameters?.installationContractDigest !== undefined && action.parameters?.installationContractDigest !== null
|
|
804
|
+
|| action.parameters?.algorithmVersion !== undefined && action.parameters?.algorithmVersion !== null;
|
|
805
|
+
const hasFieldGroup = distHasFieldGroup || actionHasFieldGroup;
|
|
806
|
+
|
|
807
|
+
if (hasFieldGroup) {
|
|
808
|
+
// 检查 distribution 字段完整性
|
|
809
|
+
if (dist.marketplaceSourceType === undefined || dist.marketplaceSourceType === null) {
|
|
810
|
+
failures.push(`unit "${unitId}", action "${action.id}": distribution missing marketplaceSourceType; new-version field group requires all fields`);
|
|
811
|
+
}
|
|
812
|
+
if (dist.installationContract === undefined || dist.installationContract === null) {
|
|
813
|
+
failures.push(`unit "${unitId}", action "${action.id}": distribution missing installationContract; new-version field group requires all fields`);
|
|
814
|
+
}
|
|
815
|
+
if (dist.installationContractDigest === undefined || dist.installationContractDigest === null) {
|
|
816
|
+
failures.push(`unit "${unitId}", action "${action.id}": distribution missing installationContractDigest; new-version field group requires all fields`);
|
|
817
|
+
}
|
|
818
|
+
// 检查 action 字段完整性
|
|
819
|
+
if (action.parameters?.marketplaceForm === undefined || action.parameters?.marketplaceForm === null) {
|
|
820
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.marketplaceForm missing; new-version field group requires all fields`);
|
|
821
|
+
}
|
|
822
|
+
if (action.parameters?.sourceDescriptor === undefined || action.parameters?.sourceDescriptor === null) {
|
|
823
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.sourceDescriptor missing; new-version field group requires all fields`);
|
|
824
|
+
}
|
|
825
|
+
if (action.parameters?.installationContractDigest === undefined || action.parameters?.installationContractDigest === null) {
|
|
826
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.installationContractDigest missing; new-version field group requires all fields`);
|
|
827
|
+
}
|
|
828
|
+
if (action.parameters?.algorithmVersion === undefined || action.parameters?.algorithmVersion === null) {
|
|
829
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.algorithmVersion missing; new-version field group requires all fields`);
|
|
830
|
+
}
|
|
831
|
+
// sourceCommit 仅生产计划必需
|
|
832
|
+
if (production && (action.parameters?.sourceCommit === undefined || action.parameters?.sourceCommit === null)) {
|
|
833
|
+
failures.push(`unit "${unitId}", action "${action.id}": parameters.sourceCommit missing; production plan requires sourceCommit`);
|
|
834
|
+
}
|
|
835
|
+
// marketplaceForm、sourceDescriptor.form、distribution 的 marketplaceSourceType 三者一致
|
|
836
|
+
const mf = action.parameters?.marketplaceForm;
|
|
837
|
+
const sdForm = action.parameters?.sourceDescriptor?.form;
|
|
838
|
+
const distSourceType = dist.marketplaceSourceType;
|
|
839
|
+
if (mf && distSourceType && mf !== distSourceType) {
|
|
840
|
+
failures.push(`unit "${unitId}", action "${action.id}": marketplaceForm "${mf}" does not match distribution marketplaceSourceType "${distSourceType}"`);
|
|
841
|
+
}
|
|
842
|
+
if (sdForm && distSourceType && sdForm !== distSourceType) {
|
|
843
|
+
failures.push(`unit "${unitId}", action "${action.id}": sourceDescriptor.form "${sdForm}" does not match distribution marketplaceSourceType "${distSourceType}"`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
792
847
|
// Parameter checks
|
|
793
848
|
_checkRequired(action, 'parameters.consumer', action.parameters?.consumer, platform.id, unitId, failures);
|
|
794
849
|
_checkRequired(action, 'parameters.plugin', action.parameters?.plugin, plugin, unitId, failures);
|
|
@@ -882,6 +937,252 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
882
937
|
}
|
|
883
938
|
}
|
|
884
939
|
|
|
940
|
+
// sourceCommit: frozen plugin source commit, binding the marketplace
|
|
941
|
+
// install action to the unit's frozen snapshot commit. Both forms
|
|
942
|
+
// require it in production plans; non-production diagnostic plans may
|
|
943
|
+
// omit it (never冒充生产计划).
|
|
944
|
+
if (production) {
|
|
945
|
+
_checkRequired(action, 'parameters.sourceCommit', action.parameters?.sourceCommit, frozen?.commit, unitId, failures);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// --- sourceDescriptor validation (marketplace source identity contract) ---
|
|
949
|
+
// Validates that marketplaceForm and sourceDescriptor.form are consistent,
|
|
950
|
+
// all form-specific fields are present and non-null, pluginRepo !== marketplaceRepo
|
|
951
|
+
// for standalone-index, and no mixed/extra fields exist.
|
|
952
|
+
// Only applies to platforms that require marketplace (claude, codex).
|
|
953
|
+
// Non-automatable platforms (kimi, codebuddy) use human-attestation
|
|
954
|
+
// and carry no marketplace form or source descriptor.
|
|
955
|
+
// Legacy plans (pre-sourceDescriptor) with legacyCompatibility are tolerated.
|
|
956
|
+
if (requiresMarketplace) {
|
|
957
|
+
const sd = action.parameters?.sourceDescriptor;
|
|
958
|
+
const mf = action.parameters?.marketplaceForm;
|
|
959
|
+
if (sd === undefined || sd === null) {
|
|
960
|
+
if (!options.legacyCompatibility) {
|
|
961
|
+
failures.push(
|
|
962
|
+
`unit "${unitId}", action "${action.id}": parameters.sourceDescriptor is missing; every marketplace install action must carry a complete source descriptor`,
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
} else if (typeof sd === 'object') {
|
|
966
|
+
// 1. marketplaceForm must be present when sourceDescriptor is present
|
|
967
|
+
if (mf === undefined || mf === null) {
|
|
968
|
+
failures.push(
|
|
969
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceForm is missing but sourceDescriptor is present; form must be explicitly declared`,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
// 2. sourceDescriptor.form must match marketplaceForm
|
|
973
|
+
if (sd.form !== mf) {
|
|
974
|
+
failures.push(
|
|
975
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.form "${sd.form}" does not match parameters.marketplaceForm "${mf}"`,
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
// 3. Form-specific field completeness and non-null checks
|
|
979
|
+
// payloadDigest is allowed to be null in non-production plans
|
|
980
|
+
// (the frozen manifestDigest is only available after production freeze)
|
|
981
|
+
const BUNDLED_REQUIRED = ['form', 'repo', 'marketplaceEntry', 'pluginSubpath'];
|
|
982
|
+
const STANDALONE_REQUIRED = ['form', 'marketplaceRepo', 'marketplaceEntry', 'pluginRepo', 'sourceType'];
|
|
983
|
+
// Production-only fields that may be null in non-production plans
|
|
984
|
+
const PROD_ONLY_FIELDS = ['marketplaceCommitSha', 'ref', 'payloadDigest'];
|
|
985
|
+
|
|
986
|
+
if (sd.form === 'bundled-family') {
|
|
987
|
+
// Check bundled-family required fields are present and non-null
|
|
988
|
+
for (const field of BUNDLED_REQUIRED) {
|
|
989
|
+
if (sd[field] === undefined || sd[field] === null) {
|
|
990
|
+
failures.push(
|
|
991
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is missing or null; bundled-family form requires all identity fields to be non-null`,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
// commit must be non-null in production plans (frozen from asset)
|
|
996
|
+
if (production && (sd.commit === undefined || sd.commit === null)) {
|
|
997
|
+
failures.push(
|
|
998
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.commit is missing or null; production bundled-family plans require a frozen commit`,
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
// commit must match frozenSnapshot.commit (production binding)
|
|
1002
|
+
if (production && sd.commit !== undefined && sd.commit !== null &&
|
|
1003
|
+
frozen?.commit !== undefined && sd.commit !== frozen.commit) {
|
|
1004
|
+
failures.push(
|
|
1005
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.commit does not match frozenSnapshot.commit`,
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
// payloadDigest must be non-null in production plans
|
|
1009
|
+
if (production && (sd.payloadDigest === undefined || sd.payloadDigest === null)) {
|
|
1010
|
+
failures.push(
|
|
1011
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.payloadDigest is missing or null; production plans require a frozen payload digest`,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
// Prohibit standalone-index fields in bundled-family
|
|
1015
|
+
const STANDALONE_ONLY = ['marketplaceRepo', 'pluginRepo', 'sourceType', 'marketplaceCommitSha', 'ref'];
|
|
1016
|
+
for (const field of STANDALONE_ONLY) {
|
|
1017
|
+
if (sd[field] !== undefined) {
|
|
1018
|
+
failures.push(
|
|
1019
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is unexpected for bundled-family form; mixed fields are prohibited`,
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// repo must match the action's repo (publicRepo for bundled-family)
|
|
1024
|
+
if (sd.repo !== undefined && sd.repo !== null && sd.repo !== publicRepo) {
|
|
1025
|
+
failures.push(
|
|
1026
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.repo "${sd.repo}" does not match unit publicRepo "${publicRepo}"`,
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
// marketplaceEntry must match action.plugin
|
|
1030
|
+
if (sd.marketplaceEntry !== undefined && sd.marketplaceEntry !== null && sd.marketplaceEntry !== plugin) {
|
|
1031
|
+
failures.push(
|
|
1032
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${plugin}"`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
// payloadDigest must match manifestDigest (when both are present)
|
|
1036
|
+
if (production && sd.payloadDigest !== undefined && sd.payloadDigest !== null &&
|
|
1037
|
+
frozen?.manifestDigest !== undefined && sd.payloadDigest !== frozen.manifestDigest) {
|
|
1038
|
+
failures.push(
|
|
1039
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.payloadDigest does not match frozen manifestDigest`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
} else if (sd.form === 'standalone-index') {
|
|
1043
|
+
// Check standalone-index required fields are present and non-null
|
|
1044
|
+
for (const field of STANDALONE_REQUIRED) {
|
|
1045
|
+
if (sd[field] === undefined || sd[field] === null) {
|
|
1046
|
+
failures.push(
|
|
1047
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is missing or null; standalone-index form requires all identity fields to be non-null`,
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
// Production-only fields: must be non-null in production plans
|
|
1052
|
+
if (production) {
|
|
1053
|
+
for (const field of PROD_ONLY_FIELDS) {
|
|
1054
|
+
if (sd[field] === undefined || sd[field] === null) {
|
|
1055
|
+
failures.push(
|
|
1056
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is missing or null; production standalone-index requires all fields to be non-null`,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// Prohibit bundled-family fields in standalone-index
|
|
1062
|
+
const BUNDLED_ONLY = ['repo', 'commit', 'pluginSubpath'];
|
|
1063
|
+
for (const field of BUNDLED_ONLY) {
|
|
1064
|
+
if (sd[field] !== undefined) {
|
|
1065
|
+
failures.push(
|
|
1066
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.${field} is unexpected for standalone-index form; mixed fields are prohibited`,
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
// pluginRepo must NOT equal marketplaceRepo
|
|
1071
|
+
if (sd.pluginRepo !== undefined && sd.pluginRepo !== null &&
|
|
1072
|
+
sd.marketplaceRepo !== undefined && sd.marketplaceRepo !== null &&
|
|
1073
|
+
sd.pluginRepo === sd.marketplaceRepo) {
|
|
1074
|
+
failures.push(
|
|
1075
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.pluginRepo must not equal marketplaceRepo; the plugin repo and marketplace repo are distinct repositories`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
// marketplaceRepo must match action.repo (for standalone-index, repo is the external marketplace)
|
|
1079
|
+
if (externalMarketplace && sd.marketplaceRepo !== undefined && sd.marketplaceRepo !== null &&
|
|
1080
|
+
sd.marketplaceRepo !== dist.marketplaceRepo) {
|
|
1081
|
+
failures.push(
|
|
1082
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match distribution marketplaceRepo "${dist.marketplaceRepo}"`,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
// pluginRepo must match the unit's publicRepo
|
|
1086
|
+
if (sd.pluginRepo !== undefined && sd.pluginRepo !== null && sd.pluginRepo !== publicRepo) {
|
|
1087
|
+
failures.push(
|
|
1088
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.pluginRepo "${sd.pluginRepo}" does not match unit publicRepo "${publicRepo}"`,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
// marketplaceEntry must match action.plugin
|
|
1092
|
+
if (sd.marketplaceEntry !== undefined && sd.marketplaceEntry !== null && sd.marketplaceEntry !== plugin) {
|
|
1093
|
+
failures.push(
|
|
1094
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${plugin}"`,
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
// sourceType must be 'marketplace-entry'
|
|
1098
|
+
if (sd.sourceType !== undefined && sd.sourceType !== null && sd.sourceType !== 'marketplace-entry') {
|
|
1099
|
+
failures.push(
|
|
1100
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.sourceType "${sd.sourceType}" is unexpected; standalone-index sourceType must be "marketplace-entry"`,
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
// payloadDigest must match manifestDigest (when both are present)
|
|
1104
|
+
if (production && sd.payloadDigest !== undefined && sd.payloadDigest !== null &&
|
|
1105
|
+
frozen?.manifestDigest !== undefined && sd.payloadDigest !== frozen.manifestDigest) {
|
|
1106
|
+
failures.push(
|
|
1107
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.payloadDigest does not match frozen manifestDigest`,
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
failures.push(
|
|
1112
|
+
`unit "${unitId}", action "${action.id}": sourceDescriptor.form "${sd.form}" is unknown; expected "bundled-family" or "standalone-index"`,
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// --- installationContractDigest 重新计算验证 ---
|
|
1119
|
+
// 不能只比较两个可一起篡改的字符串;必须重新由 distribution 的 installationContract 计算并验证
|
|
1120
|
+
if (hasFieldGroup && dist.installationContract) {
|
|
1121
|
+
// 从 distribution 的 installationContract 重新计算摘要
|
|
1122
|
+
const recomputedDigest = sha256Hex(canonicalJson(dist.installationContract));
|
|
1123
|
+
const actionDigest = action.parameters?.installationContractDigest;
|
|
1124
|
+
const distDigest = dist.installationContractDigest;
|
|
1125
|
+
|
|
1126
|
+
// 重算值必须同时等于 distribution 摘要和 action 摘要
|
|
1127
|
+
if (distDigest && recomputedDigest !== distDigest) {
|
|
1128
|
+
failures.push(
|
|
1129
|
+
`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`,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
if (actionDigest && recomputedDigest !== actionDigest) {
|
|
1133
|
+
failures.push(
|
|
1134
|
+
`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`,
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
// algorithmVersion 必须等于当前算法版本
|
|
1138
|
+
if (action.parameters?.algorithmVersion !== undefined && action.parameters?.algorithmVersion !== INSTALLATION_CONTRACT_ALGORITHM_VERSION) {
|
|
1139
|
+
failures.push(
|
|
1140
|
+
`unit "${unitId}", action "${action.id}": algorithmVersion is "${action.parameters?.algorithmVersion}", expected "${INSTALLATION_CONTRACT_ALGORITHM_VERSION}"`,
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// --- standalone-index 审计字段校验 ---
|
|
1146
|
+
// 仅 production === true 且来源为 standalone-index 时,要求 action 完整携带
|
|
1147
|
+
// 非空 marketplaceIndexPath / marketplaceName / selectedEntry。
|
|
1148
|
+
// 非生产计划不得要求这三个字段存在(未冻结时无真实值可用),
|
|
1149
|
+
// 也不得接受"用 null 表示存在"的方案。
|
|
1150
|
+
if (production && externalMarketplace && dist.marketplaceSourceType === 'standalone-index') {
|
|
1151
|
+
const params = action.parameters;
|
|
1152
|
+
if (params) {
|
|
1153
|
+
if (!params.marketplaceIndexPath) {
|
|
1154
|
+
failures.push(
|
|
1155
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceIndexPath is required for production standalone-index`,
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (!params.marketplaceName) {
|
|
1159
|
+
failures.push(
|
|
1160
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceName is required for production standalone-index`,
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (!params.selectedEntry) {
|
|
1164
|
+
failures.push(
|
|
1165
|
+
`unit "${unitId}", action "${action.id}": parameters.selectedEntry is required for production standalone-index`,
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// --- bundled-family 禁止混入 standalone 字段 ---
|
|
1172
|
+
if (!externalMarketplace && dist.marketplaceSourceType === 'bundled-family') {
|
|
1173
|
+
const params = action.parameters;
|
|
1174
|
+
if (params) {
|
|
1175
|
+
const STANDALONE_ONLY = ['marketplaceIndexPath', 'marketplaceName', 'selectedEntry'];
|
|
1176
|
+
for (const field of STANDALONE_ONLY) {
|
|
1177
|
+
if (params[field] !== undefined) {
|
|
1178
|
+
failures.push(
|
|
1179
|
+
`unit "${unitId}", action "${action.id}": parameters.${field} is unexpected for bundled-family form; mixed fields are prohibited`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
885
1186
|
// Expected checks. Marketplace identity is bound only on platforms
|
|
886
1187
|
// whose schema requires it (MINOR-1): a non-marketplace platform's
|
|
887
1188
|
// observation never binds a marketplace.
|