release-skill 0.2.3 → 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 (53) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +22 -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 +264 -913
  11. package/README.zh-CN.md +222 -535
  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 +19054 -17573
  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 +19054 -17573
  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 +19054 -17573
  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 +19054 -17573
  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 +19054 -17573
  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 +1190 -435
  39. package/src/commands/prepare.mjs +380 -40
  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 +304 -20
  44. package/src/core/baseline.mjs +8 -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 +129 -6
  50. package/src/platforms/codebuddy.mjs +193 -280
  51. package/src/platforms/codex.mjs +369 -0
  52. package/src/platforms/kimi.mjs +164 -119
  53. package/src/platforms/registry.mjs +24 -6
@@ -172,12 +172,19 @@ async function computeWorkspaceDigest(root) {
172
172
 
173
173
  // Ask Git for unambiguous NUL-delimited names, then request the patch for
174
174
  // each exact argv path. This avoids parsing C-quoted `diff --git` headers.
175
+ //
176
+ // Per-file `git diff --binary` can exceed Node.js default ~1 MiB maxBuffer
177
+ // when generated bundles (e.g. 3.8 MiB) are modified. We use an explicit
178
+ // 64 MiB upper bound: large enough for any realistic single-file diff, small
179
+ // enough to fail closed before exhausting memory.
180
+ const DIFF_MAX_BUFFER = 64 * 1024 * 1024; // 64 MiB
181
+ const diffOpts = { ...opts, maxBuffer: DIFF_MAX_BUFFER };
175
182
  const changedPaths = splitNul(changedOut).filter((p) => !isControlPlanePath(p));
176
183
  for (const changedPath of changedPaths) {
177
184
  const { stdout: patch } = await execFile(
178
185
  'git',
179
186
  ['diff', '--no-ext-diff', '--no-textconv', '--binary', '--no-color', '--', changedPath],
180
- opts,
187
+ diffOpts,
181
188
  );
182
189
  parts.push(`UNSTAGED:${changedPath}\0${patch}`);
183
190
  }
@@ -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` and the claude/codex marketplace installs depend
70
- * on Tier 1 `create-tag` (release `--verify-tag`; install ref is the tag).
71
- * - Tier 3 `kimi-marketplace-install` depends on Tier 2 `github-release`
72
- * (its install URL points at the Release page). `codebuddy-marketplace-install`
73
- * is also a non-automatable human-attestation closure and runs in Tier 3 after
74
- * the automated writes (its install is from a unified marketplace, proven by a
75
- * human attestation rather than an automated install checkpoint).
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
+ }
@@ -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) {
@@ -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
@@ -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);
@@ -1060,6 +1115,74 @@ export function validatePlanActionCompleteness(plan, options = {}) {
1060
1115
  }
1061
1116
  }
1062
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
+
1063
1186
  // Expected checks. Marketplace identity is bound only on platforms
1064
1187
  // whose schema requires it (MINOR-1): a non-marketplace platform's
1065
1188
  // observation never binds a marketplace.