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.
Files changed (54) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +4 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +41 -0
  7. package/CONTRIBUTING.md +27 -0
  8. package/INSTALL.md +95 -139
  9. package/INSTALL.zh-CN.md +70 -121
  10. package/README.md +265 -916
  11. package/README.zh-CN.md +222 -537
  12. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  13. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  14. package/adapters/claude/bin/release-skill.bundle.mjs +18710 -16694
  15. package/adapters/claude/schemas/release-plan.schema.json +137 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +70 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +18710 -16694
  20. package/adapters/codex/schemas/release-plan.schema.json +137 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +70 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +18710 -16694
  25. package/adapters/kimi/schemas/release-plan.schema.json +137 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +70 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +18710 -16694
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +137 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +70 -2
  33. package/bin/release-skill.bundle.mjs +18710 -16694
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +137 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +70 -2
  38. package/src/adapters/plugin-marketplace.mjs +1602 -605
  39. package/src/commands/prepare.mjs +441 -32
  40. package/src/commands/publish.mjs +107 -75
  41. package/src/commands/reconcile.mjs +92 -327
  42. package/src/commands/setup.mjs +148 -20
  43. package/src/commands/verify.mjs +315 -25
  44. package/src/core/baseline.mjs +21 -1
  45. package/src/core/checkpoints.mjs +50 -7
  46. package/src/core/config.mjs +15 -0
  47. package/src/core/errors.mjs +2 -0
  48. package/src/core/installation-contract.mjs +341 -0
  49. package/src/core/plan.mjs +307 -6
  50. package/src/platforms/codebuddy.mjs +191 -238
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +180 -4
  54. 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. Only platforms with a marketplace
782
- // add capability (marketplaceRefForm !== null: claude=name, codex=sha)
783
- // may carry it; kimi/codebuddy fail closed. Inline form (no
784
- // marketplaceRepo) keeps every assertion below byte-for-byte.
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 && platform.marketplaceRefForm === null) {
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 the platform has no marketplace add capability`,
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.