release-skill 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) 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 +46 -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 +266 -913
  11. package/README.zh-CN.md +224 -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 +19805 -17556
  15. package/adapters/claude/schemas/release-plan.schema.json +228 -0
  16. package/adapters/claude/schemas/release-project.schema.json +93 -0
  17. package/adapters/claude/schemas/release-run.schema.json +155 -2
  18. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  19. package/adapters/codex/bin/release-skill.bundle.mjs +19805 -17556
  20. package/adapters/codex/schemas/release-plan.schema.json +228 -0
  21. package/adapters/codex/schemas/release-project.schema.json +93 -0
  22. package/adapters/codex/schemas/release-run.schema.json +155 -2
  23. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  24. package/adapters/kimi/bin/release-skill.bundle.mjs +19805 -17556
  25. package/adapters/kimi/schemas/release-plan.schema.json +228 -0
  26. package/adapters/kimi/schemas/release-project.schema.json +93 -0
  27. package/adapters/kimi/schemas/release-run.schema.json +155 -2
  28. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
  29. package/adapters/workbuddy/bin/release-skill.bundle.mjs +19805 -17556
  30. package/adapters/workbuddy/schemas/release-plan.schema.json +228 -0
  31. package/adapters/workbuddy/schemas/release-project.schema.json +93 -0
  32. package/adapters/workbuddy/schemas/release-run.schema.json +155 -2
  33. package/bin/release-skill.bundle.mjs +19805 -17556
  34. package/package.json +1 -1
  35. package/schemas/release-plan.schema.json +228 -0
  36. package/schemas/release-project.schema.json +93 -0
  37. package/schemas/release-run.schema.json +155 -2
  38. package/scripts/sync-public-files.mjs +20 -9
  39. package/src/adapters/plugin-marketplace.mjs +1237 -403
  40. package/src/commands/prepare.mjs +454 -40
  41. package/src/commands/publish.mjs +169 -75
  42. package/src/commands/reconcile.mjs +92 -327
  43. package/src/commands/setup.mjs +148 -20
  44. package/src/commands/verify.mjs +454 -20
  45. package/src/core/baseline.mjs +8 -1
  46. package/src/core/checkpoints.mjs +50 -7
  47. package/src/core/config.mjs +15 -0
  48. package/src/core/errors.mjs +2 -0
  49. package/src/core/installation-contract.mjs +341 -0
  50. package/src/core/plan.mjs +158 -6
  51. package/src/core/skill-resource-closure.mjs +425 -0
  52. package/src/platforms/codebuddy.mjs +206 -280
  53. package/src/platforms/codex.mjs +369 -0
  54. package/src/platforms/kimi.mjs +191 -119
  55. package/src/platforms/registry.mjs +24 -6
@@ -29,13 +29,17 @@ import {
29
29
  writeEvidenceAtomic,
30
30
  } from './contract.mjs';
31
31
 
32
+ import {
33
+ computeInstallationContractDigest,
34
+ INSTALLATION_CONTRACT_ALGORITHM_VERSION,
35
+ } from '../core/installation-contract.mjs';
36
+
32
37
  import { createHash } from 'node:crypto';
33
38
  import { computeFrozenSnapshot, resolveFrozenPath } from '../snapshot/frozen.mjs';
34
39
  import { PLATFORMS, getPlatform, resolvePlatformRoute, resolveCapabilityConflicts } from '../platforms/registry.mjs';
35
40
  import {
36
41
  KIMI_REQUIREMENT_FILE,
37
42
  KIMI_ATTESTATION_FILE,
38
- KIMI_MANAGED_SUBPATH,
39
43
  resolveBoundPlanDigest,
40
44
  kimiAuthorityDir,
41
45
  validateKimiAttestation,
@@ -48,10 +52,15 @@ import {
48
52
  CODEBUDDY_MARKETPLACE_SOURCE,
49
53
  resolveCodeBuddyBoundPlanDigest,
50
54
  codebuddyAuthorityDir,
51
- codebuddyCliHome,
52
- codebuddyCliInstallRoot,
53
55
  validateCodeBuddyAttestation,
54
56
  } from '../platforms/codebuddy.mjs';
57
+ import {
58
+ CODEX_REQUIREMENT_FILE,
59
+ CODEX_ATTESTATION_FILE,
60
+ resolveCodexBoundPlanDigest,
61
+ codexAuthorityDir,
62
+ validateCodexAttestation,
63
+ } from '../platforms/codex.mjs';
55
64
 
56
65
  const execFile = promisify(execFileCb);
57
66
 
@@ -92,6 +101,9 @@ const EXTRA_INSTALLED_PATHS_CAP = 200;
92
101
  /** Diagnostic cap: at most this many conflict paths are listed per error. */
93
102
  const PAYLOAD_CONFLICT_REPORT_CAP = 10;
94
103
 
104
+ /** 消费端安装验证配方版本(与 prepare.mjs 一致)。 */
105
+ const CONSUMER_INSTALL_RECIPE_VERSION = 'consumer-install-v1';
106
+
95
107
  // Consumer-owned transport metadata written into the plugin install root
96
108
  // that is not part of the published payload (e.g. codex's root `.git`
97
109
  // checkout and `.codex-plugin/migrated-command-skills/`, claude's `.in_use`
@@ -100,6 +112,58 @@ const PAYLOAD_CONFLICT_REPORT_CAP = 10;
100
112
  // applies that list; declared-manifest-v1 verification never excludes
101
113
  // anything — host-added files are recorded as `extraInstalledPaths` instead.
102
114
 
115
+ /**
116
+ * Resolve the marketplace root directory within a snapshot.
117
+ *
118
+ * For bundled-family layouts, the marketplace root is the directory that
119
+ * contains the platform's `.claude-plugin/marketplace.json` (or equivalent).
120
+ * Entry `source` paths in the marketplace index are relative to this root.
121
+ *
122
+ * Resolution rules:
123
+ * 1. If no explicit marketplaceIndexPath is provided, the root is "." (root layout).
124
+ * 2. If marketplaceIndexPath equals the platform default, the root is ".".
125
+ * 3. If marketplaceIndexPath ends with "/" + platform default (exact suffix match),
126
+ * the prefix is the marketplace root.
127
+ * 4. Otherwise, throws: the path is not a valid marketplace index path.
128
+ *
129
+ * Uses exact suffix matching only; never uses string `includes` to guess.
130
+ *
131
+ * @param {object} platform - Platform descriptor from the registry.
132
+ * @param {string} [marketplaceIndexPath] - Explicit marketplace index path override.
133
+ * @returns {string} Normalized marketplace root ("." for root layout).
134
+ * @throws {Error} If marketplaceIndexPath is provided but doesn't match expectations.
135
+ */
136
+ export function resolveMarketplaceRoot(platform, marketplaceIndexPath) {
137
+ const defaultMarketplace = platform.manifestPaths.marketplace;
138
+ if (!defaultMarketplace) {
139
+ // Platform has no marketplace (kimi) — root concept doesn't apply.
140
+ return '.';
141
+ }
142
+
143
+ if (!marketplaceIndexPath || marketplaceIndexPath === defaultMarketplace) {
144
+ // No override or same as default → root layout.
145
+ return '.';
146
+ }
147
+
148
+ // Exact suffix match: marketplaceIndexPath must end with "/" + defaultMarketplace
149
+ const suffix = `/${defaultMarketplace}`;
150
+ if (marketplaceIndexPath.endsWith(suffix)) {
151
+ const root = marketplaceIndexPath.slice(0, -suffix.length);
152
+ if (root.length === 0) return '.';
153
+ // Validate root is a safe relative path (no escape)
154
+ if (root.startsWith('/') || root.includes('..') || root.includes('\\')) {
155
+ throw new Error(
156
+ `derived marketplace root "${root}" from marketplaceIndexPath "${marketplaceIndexPath}" is not a safe relative path`,
157
+ );
158
+ }
159
+ return root;
160
+ }
161
+
162
+ throw new Error(
163
+ `marketplaceIndexPath "${marketplaceIndexPath}" does not end with platform default marketplace path "${defaultMarketplace}"`,
164
+ );
165
+ }
166
+
103
167
  /**
104
168
  * Extract the marketplace plugin entry's declared source as a validated,
105
169
  * normalized snapshot-relative subpath ("." for root layouts).
@@ -139,6 +203,226 @@ function extractDeclaredPluginSource(consumer, entry) {
139
203
  return segments.length === 0 ? '.' : segments.join('/');
140
204
  }
141
205
 
206
+ /**
207
+ * 从市场索引中解析插件根目录并读取插件 manifest。
208
+ *
209
+ * 单一权威来源解析逻辑:
210
+ * 1. 从 marketIndex.plugins 中按 pluginName 筛选唯一匹配条目
211
+ * 2. 使用 extractDeclaredPluginSource 安全解析条目 source 字段
212
+ * 3. 将 source 安全拼接到 marketplaceRoot(快照绝对路径)并做逃逸检查
213
+ * 4. 在解析后的插件根目录下读取 platform.manifestPaths.plugin
214
+ *
215
+ * Kimi(无市场)返回 null。其他平台在市场索引为空或条目不唯一时失败关闭。
216
+ *
217
+ * @param {object|null} marketIndex - 市场索引对象(null 表示 Kimi 无市场)
218
+ * @param {string} pluginName - 要查找的插件名称
219
+ * @param {object} platform - 平台注册表条目
220
+ * @param {string} snapshotDir - 快照目录绝对路径
221
+ * @param {string} [marketplaceRootRel] - 市场根相对路径("." 或如 "adapters/claude");
222
+ * 默认 "."(根布局)。条目 source 相对此路径解析,而非快照根。
223
+ * @returns {Promise<{manifest: object, manifestRelativePath: string, pluginRoot: string}|null>}
224
+ */
225
+ export async function resolvePluginManifestFromMarketplaceEntrySource(
226
+ marketIndex,
227
+ pluginName,
228
+ platform,
229
+ snapshotDir,
230
+ marketplaceRootRel = '.',
231
+ ) {
232
+ // Kimi 无市场索引:返回 null(调用方使用平台级 manifest 规则)
233
+ if (platform.manifestPaths.marketplace === null && !marketIndex) {
234
+ return null;
235
+ }
236
+
237
+ if (!marketIndex || typeof marketIndex !== 'object') {
238
+ throw new Error('marketplace index is required for non-kimi platforms');
239
+ }
240
+
241
+ const plugins = Array.isArray(marketIndex.plugins) ? marketIndex.plugins : [];
242
+ const matches = plugins.filter((entry) => entry && entry.name === pluginName);
243
+ if (matches.length !== 1) {
244
+ throw new Error(
245
+ `marketplace index must contain exactly one plugin entry named "${pluginName}", found ${matches.length}`,
246
+ );
247
+ }
248
+
249
+ const entry = matches[0];
250
+ const sourcePath = extractDeclaredPluginSource(platform.id, entry);
251
+
252
+ // 安全拼接:source 路径已在 extractDeclaredPluginSource 中验证无逃逸。
253
+ // source 相对市场根解析(marketplaceRootRel),而非快照根。
254
+ // 这里再做 realpath 二次确认防止符号链接逃逸。
255
+ // macOS 的 /var -> /private/var 符号链接会导致 snapshotDir 与 realpath 不同,
256
+ // 因此必须先将 snapshotDir 解析为真实路径再做 containment 检查。
257
+ const snapshotDirReal = await realpath(snapshotDir).catch(() => snapshotDir);
258
+ const marketplaceRootAbs = marketplaceRootRel === '.'
259
+ ? snapshotDirReal
260
+ : resolve(snapshotDirReal, marketplaceRootRel);
261
+ const pluginRootAbs = resolve(marketplaceRootAbs, sourcePath);
262
+ const pluginRootReal = await realpath(pluginRootAbs).catch(() => null);
263
+ if (!pluginRootReal) {
264
+ throw new Error(`marketplace plugin entry source "${sourcePath}" does not exist in snapshot`);
265
+ }
266
+
267
+ // 逃逸检查:解析后的插件根必须在快照目录内
268
+ const containment = relative(snapshotDirReal, pluginRootReal);
269
+ const sep = process.platform === 'win32' ? '\\' : '/';
270
+ if (
271
+ containment !== '' &&
272
+ (isAbsolute(containment) || containment === '..' || containment.startsWith(`..${sep}`))
273
+ ) {
274
+ throw new Error(`marketplace plugin entry source "${sourcePath}" escapes the snapshot after symlink resolution`);
275
+ }
276
+
277
+ // 读取插件 manifest
278
+ const manifestRelative = platform.manifestPaths.plugin;
279
+ const manifestAbs = resolve(pluginRootReal, manifestRelative);
280
+
281
+ // 计算 manifestRelativePath:相对快照根的完整路径
282
+ const pluginRootRelToSnapshot = relative(snapshotDirReal, pluginRootReal);
283
+ const fullManifestRelative = pluginRootRelToSnapshot === ''
284
+ ? manifestRelative
285
+ : `${pluginRootRelToSnapshot}/${manifestRelative}`;
286
+
287
+ let raw;
288
+ try {
289
+ raw = await readFile(manifestAbs, 'utf8');
290
+ } catch (err) {
291
+ throw new Error(
292
+ `plugin manifest not found at "${fullManifestRelative}": ${err.message}`,
293
+ );
294
+ }
295
+
296
+ let manifest;
297
+ try {
298
+ manifest = JSON.parse(raw);
299
+ } catch (err) {
300
+ throw new Error(
301
+ `plugin manifest at "${fullManifestRelative}" is not valid JSON: ${err.message}`,
302
+ );
303
+ }
304
+
305
+ return {
306
+ manifest,
307
+ manifestRelativePath: fullManifestRelative,
308
+ pluginRoot: pluginRootRelToSnapshot || '.',
309
+ };
310
+ }
311
+
312
+ /**
313
+ * 验证 installationContractDigest:从冻结快照读取插件 manifest,
314
+ * 按平台注册表和来源形态构建安装契约,以 consumer-install-v1 重算摘要并比对。
315
+ *
316
+ * @param {object} action - 展开后的 action(parameters 已展开到顶层)
317
+ * @param {string} snapshotDirReal - 冻结快照的 realpath
318
+ * @param {object} platform - 平台注册表条目
319
+ * @returns {Promise<{valid: boolean, error: string|null}>}
320
+ */
321
+ async function validateInstallationContractDigest(action, snapshotDirReal, platform) {
322
+ const form = action.marketplaceSourceType;
323
+
324
+ // 1. 确定是否纳入市场条目,并获取条目(manifest 读取依赖条目 source 路径)
325
+ const hasMarketplacePath = platform.manifestPaths.marketplace !== null;
326
+ let includeMarketplaceEntry = false;
327
+ let selectedMarketplaceEntry = null;
328
+ let marketplaceIndexRelative = null;
329
+ let marketIndexParsed = null;
330
+
331
+ if (form === 'bundled-family') {
332
+ includeMarketplaceEntry = hasMarketplacePath;
333
+ if (includeMarketplaceEntry) {
334
+ // 从快照中读取 bundled 市场索引,提取唯一匹配条目。
335
+ // marketplaceIndexPath 若存在(嵌套布局),用作完整索引路径;
336
+ // 否则使用平台默认路径(根布局)。
337
+ marketplaceIndexRelative = action.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
338
+ const marketplacePath = resolve(snapshotDirReal, marketplaceIndexRelative);
339
+ const mkResult = await validateManifestFile(marketplacePath, ['name', 'plugins']);
340
+ if (!mkResult.valid) {
341
+ return { valid: false, error: `installationContractDigest 验证失败:市场索引读取失败:${mkResult.error}` };
342
+ }
343
+ const plugins = mkResult.manifest.plugins;
344
+ if (!Array.isArray(plugins)) {
345
+ return { valid: false, error: `installationContractDigest 验证失败:市场索引 plugins 非数组` };
346
+ }
347
+ const matches = plugins.filter((p) => p && p.name === action.plugin);
348
+ if (matches.length !== 1) {
349
+ return { valid: false, error: `installationContractDigest 验证失败:市场索引中匹配 "${action.plugin}" 的条目数量为 ${matches.length}` };
350
+ }
351
+ selectedMarketplaceEntry = matches[0];
352
+ marketIndexParsed = mkResult.manifest;
353
+ }
354
+ } else if (form === 'standalone-index') {
355
+ // 独立市场:Kimi 不纳入市场条目(Kimi 无市场 CLI,selectedEntry
356
+ // 仅供静态校验独立市场身份字段,不进入安装契约摘要)。
357
+ // Claude/Codex/CodeBuddy 使用 action 中的 selectedEntry 和 marketplaceIndexPath。
358
+ if (platform.id !== 'kimi' && action.selectedEntry && action.marketplaceIndexPath) {
359
+ includeMarketplaceEntry = true;
360
+ selectedMarketplaceEntry = action.selectedEntry;
361
+ marketplaceIndexRelative = action.marketplaceIndexPath;
362
+ }
363
+ }
364
+
365
+ // 2. 读取插件 manifest:使用市场条目 source 路径解析(单一权威 helper)。
366
+ // bundled-family + 有市场索引:通过 resolvePluginManifestFromMarketplaceEntrySource
367
+ // 从条目 source 安全解析插件根并读取 manifest。
368
+ // 其他路径:保留原有策略。
369
+ let pluginManifestParsed;
370
+ let pluginManifestRelative;
371
+
372
+ if (form === 'bundled-family' && marketIndexParsed && platform.marketplaceSourceForm !== null) {
373
+ // bundled-family 有市场索引且平台支持市场来源解析(Claude/Codex):
374
+ // 使用权威 helper 从条目 source 解析。
375
+ // 计算市场根:从 marketplaceIndexRelative 推断(精确后缀匹配)。
376
+ const mktRoot = resolveMarketplaceRoot(platform, marketplaceIndexRelative);
377
+ try {
378
+ const resolved = await resolvePluginManifestFromMarketplaceEntrySource(
379
+ marketIndexParsed, action.plugin, platform, snapshotDirReal, mktRoot,
380
+ );
381
+ pluginManifestParsed = resolved.manifest;
382
+ pluginManifestRelative = resolved.manifestRelativePath;
383
+ } catch (err) {
384
+ return { valid: false, error: `installationContractDigest 验证失败:${err.message}` };
385
+ }
386
+ } else if (platform.strategy.readManifest) {
387
+ // Kimi/CodeBuddy/Codex 有自定义 manifest 读取策略
388
+ const readResult = await platform.strategy.readManifest(snapshotDirReal);
389
+ pluginManifestParsed = readResult.manifest;
390
+ pluginManifestRelative = readResult.manifestRelative ?? platform.manifestPaths.plugin;
391
+ } else {
392
+ // Claude fallback(standalone-index 或无市场索引时)
393
+ pluginManifestRelative = platform.manifestPaths.plugin;
394
+ const pluginManifestPath = resolve(snapshotDirReal, pluginManifestRelative);
395
+ const manifestResult = await validateManifestFile(pluginManifestPath, ['name', 'version']);
396
+ if (!manifestResult.valid) {
397
+ return { valid: false, error: `installationContractDigest 验证失败:插件 manifest 读取失败:${manifestResult.error}` };
398
+ }
399
+ pluginManifestParsed = manifestResult.manifest;
400
+ }
401
+
402
+ // 3. 使用权威构建/摘要函数重算
403
+ const computedDigest = computeInstallationContractDigest({
404
+ distributionType: platform.distributionType,
405
+ manifestRelativePath: pluginManifestRelative,
406
+ manifest: pluginManifestParsed,
407
+ marketplaceSourceType: form,
408
+ includeMarketplaceEntry,
409
+ ...(includeMarketplaceEntry ? {
410
+ marketplaceIndexRelativePath: marketplaceIndexRelative,
411
+ selectedMarketplaceEntry,
412
+ } : {}),
413
+ verificationRecipeVersion: CONSUMER_INSTALL_RECIPE_VERSION,
414
+ });
415
+
416
+ if (computedDigest !== action.installationContractDigest) {
417
+ return {
418
+ valid: false,
419
+ error: `installationContractDigest 不匹配:预期 ${action.installationContractDigest.slice(0, 16)}...,重算 ${computedDigest.slice(0, 16)}...`,
420
+ };
421
+ }
422
+
423
+ return { valid: true, error: null };
424
+ }
425
+
142
426
  /**
143
427
  * Resolve the payload subpath the consumer CLI installs for this action,
144
428
  * read from the marketplace manifest inside the digest-verified frozen
@@ -564,6 +848,49 @@ const SUPPORTED_TYPES = [
564
848
  ActionType.CODEBUDDY_MARKETPLACE_INSTALL,
565
849
  ];
566
850
 
851
+ /**
852
+ * Classify whether an error indicates CLI/transport unavailability.
853
+ *
854
+ * ONLY these errors trigger human-attestation fallback for Codex:
855
+ * - ENOENT: binary not found
856
+ * - ETIMEDOUT/ECONNREFUSED/ECONNRESET: connection errors
857
+ * - ENOTFOUND: DNS resolution failure
858
+ * - spawn errors with specific codes
859
+ *
860
+ * All other errors (identity mismatch, malformed JSON, program bugs)
861
+ * are hard failures and NEVER trigger fallback.
862
+ *
863
+ * @param {Error} err
864
+ * @returns {boolean}
865
+ */
866
+ function isCliOrTransportUnavailable(err) {
867
+ if (!err || typeof err !== 'object') return false;
868
+
869
+ // Node.js system error codes for CLI/transport unavailability
870
+ const UNAVAILABILITY_CODES = new Set([
871
+ 'ENOENT', // binary not found
872
+ 'ETIMEDOUT', // connection timeout
873
+ 'ECONNREFUSED', // connection refused
874
+ 'ECONNRESET', // connection reset
875
+ 'ENOTFOUND', // DNS resolution failure
876
+ 'EAI_AGAIN', // DNS temporary failure
877
+ 'EHOSTUNREACH', // host unreachable
878
+ 'ENETUNREACH', // network unreachable
879
+ ]);
880
+
881
+ if (err.code && UNAVAILABILITY_CODES.has(err.code)) {
882
+ return true;
883
+ }
884
+
885
+ // Check error message for spawn/ENOENT patterns
886
+ const msg = err.message || '';
887
+ if (msg.includes('ENOENT') || msg.includes('spawn')) {
888
+ return true;
889
+ }
890
+
891
+ return false;
892
+ }
893
+
567
894
  /** Safe repo pattern: owner/repo with alphanumeric, hyphens, dots, underscores. */
568
895
  const SAFE_REPO_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
569
896
 
@@ -736,6 +1063,98 @@ async function checkRequiredFiles(dir, requiredFiles) {
736
1063
  return { allPresent: missing.length === 0, missing };
737
1064
  }
738
1065
 
1066
+ // ---------------------------------------------------------------------------
1067
+ // 市场来源单选模型(DT-D)
1068
+ // ---------------------------------------------------------------------------
1069
+
1070
+ /**
1071
+ * 市场来源类型常量。
1072
+ *
1073
+ * - BUNDLED_FAMILY: 技能族自带市场文件(marketplace.json 与插件代码在同一仓库)
1074
+ * - STANDALONE_INDEX: 独立市场仓库(市场索引在外部仓库,插件代码在另一仓库)
1075
+ */
1076
+ export const MARKETPLACE_SOURCE_TYPES = Object.freeze({
1077
+ BUNDLED_FAMILY: 'bundled-family',
1078
+ STANDALONE_INDEX: 'standalone-index',
1079
+ });
1080
+
1081
+ /**
1082
+ * 每个平台支持的市场来源类型。
1083
+ *
1084
+ * 所有平台均支持 bundled-family 和 standalone-index 两种来源形态。
1085
+ * 不得硬编码某个平台只能使用其中一种。
1086
+ */
1087
+ const PLATFORM_SUPPORTED_SOURCES = Object.freeze({
1088
+ claude: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
1089
+ codex: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
1090
+ kimi: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
1091
+ codebuddy: new Set([MARKETPLACE_SOURCE_TYPES.BUNDLED_FAMILY, MARKETPLACE_SOURCE_TYPES.STANDALONE_INDEX]),
1092
+ });
1093
+
1094
+ /**
1095
+ * 验证市场来源单选约束:每个平台每次发布只允许一种来源。
1096
+ *
1097
+ * 根据平台能力和配置/计划中的市场来源类型进行校验:
1098
+ * 1. 混用两种来源失败关闭
1099
+ * 2. 不支持的来源类型失败关闭
1100
+ *
1101
+ * @param {string} platform - 平台标识(claude / codex / kimi / codebuddy)
1102
+ * @param {object} config - 项目配置(releaseUnit.distributions 中的一项)
1103
+ * @param {object} plan - 发布计划(units.distributions 中的一项)
1104
+ * @returns {{ valid: boolean, error: string|null, selectedSource: string|null }}
1105
+ */
1106
+ export function validateMarketplaceSourceSelection(platform, config, plan) {
1107
+ // 每个平台只允许一种来源
1108
+ const configSource = config?.marketplaceSourceType ?? null;
1109
+ const planSource = plan?.marketplaceSourceType ?? null;
1110
+
1111
+ // 确定选定的来源类型
1112
+ const selectedSource = planSource ?? configSource;
1113
+
1114
+ if (configSource && planSource && configSource !== planSource) {
1115
+ return {
1116
+ valid: false,
1117
+ error: `平台 "${platform}" 市场来源混用:配置声明 "${configSource}",计划声明 "${planSource}";每次发布只允许一种来源`,
1118
+ selectedSource: null,
1119
+ };
1120
+ }
1121
+
1122
+ // 如果没有声明来源类型,无法验证(向后兼容旧配置)
1123
+ if (!selectedSource) {
1124
+ return { valid: true, error: null, selectedSource: null };
1125
+ }
1126
+
1127
+ // 检查来源类型是否为已知值
1128
+ const knownSources = new Set(Object.values(MARKETPLACE_SOURCE_TYPES));
1129
+ if (!knownSources.has(selectedSource)) {
1130
+ return {
1131
+ valid: false,
1132
+ error: `未知的市场来源类型 "${selectedSource}";合法值为 ${[...knownSources].join(', ')}`,
1133
+ selectedSource: null,
1134
+ };
1135
+ }
1136
+
1137
+ // 检查平台是否支持该来源类型
1138
+ const supportedSources = PLATFORM_SUPPORTED_SOURCES[platform];
1139
+ if (!supportedSources) {
1140
+ return {
1141
+ valid: false,
1142
+ error: `未知的平台 "${platform}"`,
1143
+ selectedSource: null,
1144
+ };
1145
+ }
1146
+ if (!supportedSources.has(selectedSource)) {
1147
+ const supportedList = [...supportedSources].join(', ');
1148
+ return {
1149
+ valid: false,
1150
+ error: `平台 "${platform}" 不支持市场来源 "${selectedSource}";该平台仅支持: ${supportedList}`,
1151
+ selectedSource: null,
1152
+ };
1153
+ }
1154
+
1155
+ return { valid: true, error: null, selectedSource };
1156
+ }
1157
+
739
1158
  /**
740
1159
  * Create the plugin-marketplace adapter.
741
1160
  *
@@ -915,55 +1334,88 @@ export function createPluginMarketplaceAdapter(deps = {}) {
915
1334
  });
916
1335
  }
917
1336
 
918
- // 6. sourceDescriptor validation (structured-cli route only).
919
- // Human-attestation platforms (kimi, codebuddy) are exempt: they have no
920
- // scriptable install CLI, so the source descriptor's conflict
921
- // detection is irrelevant to their manual-requirement workflow.
1337
+ // 6. 统一静态预检:四平台共同执行安装来源与安装契约校验。
1338
+ // 旧冻结计划(整组新版字段完全不存在时)保留既有路径。
1339
+ // 新版字段组(installationContractDigest、algorithmVersion、marketplaceSourceType 为标志)
1340
+ // 出现任一字段时必须完整。0.2.3 已有的 marketplaceForm、sourceDescriptor、sourceCommit
1341
+ // 单独出现不得触发新版组。
922
1342
  const route = resolvePlatformRoute(consumerPlatform);
923
- if (route.route === 'structured-cli') {
1343
+
1344
+ // 6a. 新版字段组检测:installationContractDigest、algorithmVersion、marketplaceSourceType
1345
+ // 任一存在时走新路径
1346
+ const NEW_FIELD_GROUP_MARKERS = ['installationContractDigest', 'algorithmVersion', 'marketplaceSourceType'];
1347
+ const hasNewFieldGroup = NEW_FIELD_GROUP_MARKERS.some(
1348
+ (f) => action[f] !== undefined && action[f] !== null,
1349
+ );
1350
+
1351
+ if (hasNewFieldGroup) {
1352
+ // 新版字段组完整性:任一出现则要求全组完整
1353
+ const newFields = [
1354
+ 'marketplaceForm', 'sourceDescriptor', 'installationContractDigest',
1355
+ 'algorithmVersion', 'sourceCommit', 'marketplaceSourceType',
1356
+ ];
1357
+ const missingNewFields = newFields.filter((f) => action[f] === undefined || action[f] === null);
1358
+ if (missingNewFields.length > 0) {
1359
+ return createResult({
1360
+ actionType,
1361
+ status: ActionStatus.PREFLIGHT_FAILED,
1362
+ error: `新版字段组不完整,缺失: ${missingNewFields.join(', ')};任一出现则要求全组完整`,
1363
+ });
1364
+ }
1365
+
924
1366
  const sd = action.sourceDescriptor;
925
1367
  if (!sd || typeof sd !== 'object') {
926
1368
  return createResult({
927
1369
  actionType,
928
1370
  status: ActionStatus.PREFLIGHT_FAILED,
929
- error: 'sourceDescriptor is required for marketplace install',
1371
+ error: 'sourceDescriptor is required when installationContractDigest is present',
930
1372
  });
931
1373
  }
932
1374
 
933
- // Form consistency: marketplaceForm must agree with sourceDescriptor.form.
934
- if (sd.form !== action.marketplaceForm) {
1375
+ // marketplaceForm 三方一致性
1376
+ if (action.marketplaceForm !== sd.form || action.marketplaceForm !== action.marketplaceSourceType) {
935
1377
  return createResult({
936
1378
  actionType,
937
1379
  status: ActionStatus.PREFLIGHT_FAILED,
938
- error: `sourceDescriptor.form "${sd.form}" does not match marketplaceForm "${action.marketplaceForm}"`,
1380
+ error: `marketplaceForm "${action.marketplaceForm}"、sourceDescriptor.form "${sd.form}"、marketplaceSourceType "${action.marketplaceSourceType}" 三者必须一致`,
939
1381
  });
940
1382
  }
941
1383
 
942
- // payloadDigest: must be a valid 64-char lowercase hex string and
943
- // must not be the null hash (all zeros).
944
- if (typeof sd.payloadDigest !== 'string' || sd.payloadDigest.length === 0) {
1384
+ // installationContractDigest 格式
1385
+ if (!SAFE_DIGEST_RE.test(action.installationContractDigest)) {
945
1386
  return createResult({
946
1387
  actionType,
947
1388
  status: ActionStatus.PREFLIGHT_FAILED,
948
- error: 'sourceDescriptor.payloadDigest is required',
1389
+ error: 'installationContractDigest must be a 64-char lowercase hex string',
949
1390
  });
950
1391
  }
951
- if (!/^[a-f0-9]{64}$/.test(sd.payloadDigest)) {
1392
+
1393
+ // algorithmVersion
1394
+ if (action.algorithmVersion !== INSTALLATION_CONTRACT_ALGORITHM_VERSION) {
952
1395
  return createResult({
953
1396
  actionType,
954
1397
  status: ActionStatus.PREFLIGHT_FAILED,
955
- error: 'sourceDescriptor.payloadDigest must be a 64-char lowercase hex string',
1398
+ error: `algorithmVersion must be ${INSTALLATION_CONTRACT_ALGORITHM_VERSION}, got ${action.algorithmVersion}`,
956
1399
  });
957
1400
  }
958
- if (sd.payloadDigest === '0'.repeat(64)) {
1401
+
1402
+ // sourceCommit 格式
1403
+ if (typeof action.sourceCommit !== 'string' || !/^[0-9a-f]{40}$/.test(action.sourceCommit)) {
959
1404
  return createResult({
960
1405
  actionType,
961
1406
  status: ActionStatus.PREFLIGHT_FAILED,
962
- error: 'sourceDescriptor.payloadDigest must not be the null hash',
1407
+ error: 'sourceCommit must be a 40-char lowercase hex commit sha',
963
1408
  });
964
1409
  }
965
1410
 
966
- // marketplaceEntry: must match the action plugin name.
1411
+ // sourceDescriptor 共通字段
1412
+ if (typeof sd.form !== 'string' || (sd.form !== 'bundled-family' && sd.form !== 'standalone-index')) {
1413
+ return createResult({
1414
+ actionType,
1415
+ status: ActionStatus.PREFLIGHT_FAILED,
1416
+ error: `sourceDescriptor.form must be "bundled-family" or "standalone-index", got "${sd.form}"`,
1417
+ });
1418
+ }
967
1419
  if (typeof sd.marketplaceEntry !== 'string' || sd.marketplaceEntry.length === 0) {
968
1420
  return createResult({
969
1421
  actionType,
@@ -979,13 +1431,14 @@ export function createPluginMarketplaceAdapter(deps = {}) {
979
1431
  });
980
1432
  }
981
1433
 
982
- // Form-specific field validation.
1434
+ // 来源形态专属字段校验
983
1435
  if (sd.form === 'bundled-family') {
1436
+ // bundled-family 完整性
984
1437
  if (!sd.repo || !SAFE_REPO_RE.test(sd.repo)) {
985
1438
  return createResult({
986
1439
  actionType,
987
1440
  status: ActionStatus.PREFLIGHT_FAILED,
988
- error: `sourceDescriptor.repo is required and must be a safe repo pattern`,
1441
+ error: 'sourceDescriptor.repo is required and must be a safe repo pattern for bundled-family form',
989
1442
  });
990
1443
  }
991
1444
  if (sd.repo !== action.repo) {
@@ -995,37 +1448,32 @@ export function createPluginMarketplaceAdapter(deps = {}) {
995
1448
  error: `sourceDescriptor.repo "${sd.repo}" does not match action.repo "${action.repo}"`,
996
1449
  });
997
1450
  }
998
- // commit 可选,但如果存在则必须是 40-hex 格式
999
- if (sd.commit !== undefined && (typeof sd.commit !== 'string' || !/^[0-9a-f]{40}$/.test(sd.commit))) {
1451
+ if (typeof sd.commit !== 'string' || !/^[0-9a-f]{40}$/.test(sd.commit)) {
1000
1452
  return createResult({
1001
1453
  actionType,
1002
1454
  status: ActionStatus.PREFLIGHT_FAILED,
1003
1455
  error: 'sourceDescriptor.commit must be a 40-hex commit sha for bundled-family form',
1004
1456
  });
1005
1457
  }
1006
- // commit 交叉校验:bundled sourceDescriptor.commit 必须与
1007
- // action.sourceCommit(冻结的插件来源提交)一致
1008
- if (sd.commit !== undefined && action.sourceCommit && sd.commit !== action.sourceCommit) {
1458
+ if (sd.commit !== action.sourceCommit) {
1009
1459
  return createResult({
1010
1460
  actionType,
1011
1461
  status: ActionStatus.PREFLIGHT_FAILED,
1012
1462
  error: `sourceDescriptor.commit "${sd.commit}" does not match action.sourceCommit "${action.sourceCommit}"`,
1013
1463
  });
1014
1464
  }
1015
- // payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
1016
- if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
1465
+ if (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest)) {
1017
1466
  return createResult({
1018
1467
  actionType,
1019
1468
  status: ActionStatus.PREFLIGHT_FAILED,
1020
- error: 'sourceDescriptor.payloadDigest is required for bundled-family form',
1469
+ error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for bundled-family form',
1021
1470
  });
1022
1471
  }
1023
- // payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
1024
- if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
1472
+ if (sd.payloadDigest !== action.manifestDigest) {
1025
1473
  return createResult({
1026
1474
  actionType,
1027
1475
  status: ActionStatus.PREFLIGHT_FAILED,
1028
- error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
1476
+ error: 'sourceDescriptor.payloadDigest does not match action.manifestDigest',
1029
1477
  });
1030
1478
  }
1031
1479
  if (typeof sd.pluginSubpath !== 'string' || sd.pluginSubpath.length === 0) {
@@ -1035,14 +1483,48 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1035
1483
  error: 'sourceDescriptor.pluginSubpath is required for bundled-family form',
1036
1484
  });
1037
1485
  }
1038
- } else if (sd.form === 'standalone-index') {
1039
- if (!sd.pluginRepo || !SAFE_REPO_RE.test(sd.pluginRepo)) {
1486
+ // 不允许独立市场专属字段(sourceDescriptor 层)
1487
+ if (sd.marketplaceRepo !== undefined || sd.marketplaceCommitSha !== undefined || sd.pluginRepo !== undefined) {
1488
+ return createResult({
1489
+ actionType,
1490
+ status: ActionStatus.PREFLIGHT_FAILED,
1491
+ error: 'bundled-family sourceDescriptor must not contain standalone-index fields (marketplaceRepo, marketplaceCommitSha, pluginRepo)',
1492
+ });
1493
+ }
1494
+ // 不允许 action 顶层独立市场专属字段,避免双来源混用。
1495
+ // marketplaceIndexPath 对 bundled-family 嵌套布局也合法(指示市场根位置),
1496
+ // 因此仅禁止真正的 standalone-index 专属字段。
1497
+ if (action.marketplaceCommitSha !== undefined || action.marketplaceName !== undefined || action.selectedEntry !== undefined) {
1040
1498
  return createResult({
1041
1499
  actionType,
1042
1500
  status: ActionStatus.PREFLIGHT_FAILED,
1043
- error: `sourceDescriptor.pluginRepo is required for standalone-index form`,
1501
+ error: 'bundled-family action must not contain standalone-index fields (marketplaceCommitSha, marketplaceName, selectedEntry)',
1044
1502
  });
1045
1503
  }
1504
+ // bundled-family marketplaceIndexPath 格式校验(可选,嵌套布局时存在)
1505
+ if (action.marketplaceIndexPath !== undefined && action.marketplaceIndexPath !== null) {
1506
+ if (typeof action.marketplaceIndexPath !== 'string' || action.marketplaceIndexPath.length === 0) {
1507
+ return createResult({
1508
+ actionType,
1509
+ status: ActionStatus.PREFLIGHT_FAILED,
1510
+ error: 'bundled-family marketplaceIndexPath must be a non-empty string when present',
1511
+ });
1512
+ }
1513
+ if (
1514
+ action.marketplaceIndexPath.startsWith('/') ||
1515
+ action.marketplaceIndexPath.includes('..') ||
1516
+ action.marketplaceIndexPath.includes('\\') ||
1517
+ /^https?:\/\//i.test(action.marketplaceIndexPath)
1518
+ ) {
1519
+ return createResult({
1520
+ actionType,
1521
+ status: ActionStatus.PREFLIGHT_FAILED,
1522
+ error: `marketplaceIndexPath "${action.marketplaceIndexPath}" is not a safe relative path`,
1523
+ });
1524
+ }
1525
+ }
1526
+ } else {
1527
+ // standalone-index 完整性
1046
1528
  if (!sd.marketplaceRepo || !SAFE_REPO_RE.test(sd.marketplaceRepo)) {
1047
1529
  return createResult({
1048
1530
  actionType,
@@ -1050,8 +1532,13 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1050
1532
  error: 'sourceDescriptor.marketplaceRepo is required for standalone-index form',
1051
1533
  });
1052
1534
  }
1053
- // 独立市场身份交叉校验:
1054
- // 1. marketplaceRepo 必须等于 action.repo(市场仓库身份)
1535
+ if (!sd.pluginRepo || !SAFE_REPO_RE.test(sd.pluginRepo)) {
1536
+ return createResult({
1537
+ actionType,
1538
+ status: ActionStatus.PREFLIGHT_FAILED,
1539
+ error: 'sourceDescriptor.pluginRepo is required for standalone-index form',
1540
+ });
1541
+ }
1055
1542
  if (sd.marketplaceRepo !== action.repo) {
1056
1543
  return createResult({
1057
1544
  actionType,
@@ -1059,7 +1546,6 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1059
1546
  error: `sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match action.repo "${action.repo}"`,
1060
1547
  });
1061
1548
  }
1062
- // 2. pluginRepo 必须与 marketplaceRepo 不同(发布单元仓库 ≠ 市场仓库)
1063
1549
  if (sd.pluginRepo === sd.marketplaceRepo) {
1064
1550
  return createResult({
1065
1551
  actionType,
@@ -1067,22 +1553,27 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1067
1553
  error: `sourceDescriptor.pluginRepo "${sd.pluginRepo}" must differ from marketplaceRepo "${sd.marketplaceRepo}"`,
1068
1554
  });
1069
1555
  }
1070
- if (
1071
- typeof sd.marketplaceCommitSha !== 'string'
1072
- || !/^[0-9a-f]{40}$/.test(sd.marketplaceCommitSha)
1073
- ) {
1556
+ if (typeof sd.marketplaceCommitSha !== 'string' || !/^[0-9a-f]{40}$/.test(sd.marketplaceCommitSha)) {
1074
1557
  return createResult({
1075
1558
  actionType,
1076
1559
  status: ActionStatus.PREFLIGHT_FAILED,
1077
1560
  error: 'sourceDescriptor.marketplaceCommitSha must be a 40-hex commit sha for standalone-index form',
1078
1561
  });
1079
1562
  }
1080
- // 3. marketplaceCommitSha 交叉校验(与 action 层一致)
1081
- if (action.marketplaceCommitSha && sd.marketplaceCommitSha !== action.marketplaceCommitSha) {
1563
+ // action 顶层 marketplaceCommitSha 必须存在、为 40 位小写提交 SHA,
1564
+ // 且等于 sourceDescriptor.marketplaceCommitSha
1565
+ if (typeof action.marketplaceCommitSha !== 'string' || !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)) {
1566
+ return createResult({
1567
+ actionType,
1568
+ status: ActionStatus.PREFLIGHT_FAILED,
1569
+ error: 'marketplaceCommitSha is required at action top level for standalone-index form',
1570
+ });
1571
+ }
1572
+ if (sd.marketplaceCommitSha !== action.marketplaceCommitSha) {
1082
1573
  return createResult({
1083
1574
  actionType,
1084
1575
  status: ActionStatus.PREFLIGHT_FAILED,
1085
- error: `sourceDescriptor.marketplaceCommitSha "${sd.marketplaceCommitSha}" does not match action.marketplaceCommitSha "${action.marketplaceCommitSha}"`,
1576
+ error: `sourceDescriptor.marketplaceCommitSha does not match action.marketplaceCommitSha`,
1086
1577
  });
1087
1578
  }
1088
1579
  if (typeof sd.ref !== 'string' || sd.ref.length === 0) {
@@ -1092,46 +1583,320 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1092
1583
  error: 'sourceDescriptor.ref is required for standalone-index form',
1093
1584
  });
1094
1585
  }
1095
- // 4. payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
1096
- if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
1586
+ if (sd.ref !== action.ref) {
1587
+ return createResult({
1588
+ actionType,
1589
+ status: ActionStatus.PREFLIGHT_FAILED,
1590
+ error: `sourceDescriptor.ref "${sd.ref}" does not match action.ref "${action.ref}"`,
1591
+ });
1592
+ }
1593
+ // payloadDigest 必须存在、为合法摘要且等于 manifestDigest(不能可选)
1594
+ if (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest)) {
1097
1595
  return createResult({
1098
1596
  actionType,
1099
1597
  status: ActionStatus.PREFLIGHT_FAILED,
1100
1598
  error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for standalone-index form',
1101
1599
  });
1102
1600
  }
1103
- // 5. payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
1104
- if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
1601
+ if (sd.payloadDigest !== action.manifestDigest) {
1105
1602
  return createResult({
1106
1603
  actionType,
1107
1604
  status: ActionStatus.PREFLIGHT_FAILED,
1108
- error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
1605
+ error: 'sourceDescriptor.payloadDigest does not match action.manifestDigest',
1109
1606
  });
1110
1607
  }
1111
- } else {
1608
+ // 不允许 bundled-family 专属字段
1609
+ if (sd.commit !== undefined || sd.pluginSubpath !== undefined) {
1610
+ return createResult({
1611
+ actionType,
1612
+ status: ActionStatus.PREFLIGHT_FAILED,
1613
+ error: 'standalone-index sourceDescriptor must not contain bundled-family fields (commit, pluginSubpath)',
1614
+ });
1615
+ }
1616
+ }
1617
+
1618
+ // standalone-index 独立市场专属字段校验(action 层)
1619
+ if (action.marketplaceSourceType === 'standalone-index') {
1620
+ // marketplaceIndexPath 必须是安全非空相对路径
1621
+ if (typeof action.marketplaceIndexPath !== 'string' || action.marketplaceIndexPath.length === 0) {
1622
+ return createResult({
1623
+ actionType,
1624
+ status: ActionStatus.PREFLIGHT_FAILED,
1625
+ error: 'marketplaceIndexPath is required for standalone-index form',
1626
+ });
1627
+ }
1628
+ if (
1629
+ action.marketplaceIndexPath.startsWith('/') ||
1630
+ action.marketplaceIndexPath.includes('..') ||
1631
+ action.marketplaceIndexPath.includes('\\') ||
1632
+ /^https?:\/\//i.test(action.marketplaceIndexPath)
1633
+ ) {
1634
+ return createResult({
1635
+ actionType,
1636
+ status: ActionStatus.PREFLIGHT_FAILED,
1637
+ error: `marketplaceIndexPath "${action.marketplaceIndexPath}" is not a safe relative path`,
1638
+ });
1639
+ }
1640
+ // marketplaceName 必须是安全非空标识
1641
+ if (typeof action.marketplaceName !== 'string' || action.marketplaceName.length === 0) {
1642
+ return createResult({
1643
+ actionType,
1644
+ status: ActionStatus.PREFLIGHT_FAILED,
1645
+ error: 'marketplaceName is required for standalone-index form',
1646
+ });
1647
+ }
1648
+ if (!SAFE_ID_RE.test(action.marketplaceName)) {
1649
+ return createResult({
1650
+ actionType,
1651
+ status: ActionStatus.PREFLIGHT_FAILED,
1652
+ error: `unsafe marketplaceName identifier: "${action.marketplaceName}"`,
1653
+ });
1654
+ }
1655
+ // 若 action 带 marketplace,两者必须相等
1656
+ if (action.marketplace !== undefined && action.marketplace !== null && action.marketplaceName !== action.marketplace) {
1657
+ return createResult({
1658
+ actionType,
1659
+ status: ActionStatus.PREFLIGHT_FAILED,
1660
+ error: `marketplaceName "${action.marketplaceName}" does not match action.marketplace "${action.marketplace}"`,
1661
+ });
1662
+ }
1663
+ // selectedEntry 必须是普通对象,name/version 绑定 action
1664
+ if (!action.selectedEntry || typeof action.selectedEntry !== 'object' || Array.isArray(action.selectedEntry)) {
1665
+ return createResult({
1666
+ actionType,
1667
+ status: ActionStatus.PREFLIGHT_FAILED,
1668
+ error: 'selectedEntry must be a non-null object for standalone-index form',
1669
+ });
1670
+ }
1671
+ if (action.selectedEntry.name !== action.plugin) {
1672
+ return createResult({
1673
+ actionType,
1674
+ status: ActionStatus.PREFLIGHT_FAILED,
1675
+ error: `selectedEntry.name "${action.selectedEntry.name}" does not match action.plugin "${action.plugin}"`,
1676
+ });
1677
+ }
1678
+ if (action.selectedEntry.version !== undefined && action.selectedEntry.version !== action.version) {
1679
+ return createResult({
1680
+ actionType,
1681
+ status: ActionStatus.PREFLIGHT_FAILED,
1682
+ error: `selectedEntry.version "${action.selectedEntry.version}" does not match action.version "${action.version}"`,
1683
+ });
1684
+ }
1685
+ }
1686
+ } else {
1687
+ // 6b. 旧路径:sourceDescriptor 必须存在且为对象
1688
+ const sd = action.sourceDescriptor;
1689
+ if (!sd || typeof sd !== 'object') {
1112
1690
  return createResult({
1113
1691
  actionType,
1114
1692
  status: ActionStatus.PREFLIGHT_FAILED,
1115
- error: `sourceDescriptor.form "${sd.form}" is not a recognized form (expected "bundled-family" or "standalone-index")`,
1693
+ error: 'sourceDescriptor is required for marketplace install actions',
1116
1694
  });
1117
1695
  }
1118
- }
1696
+ {
1697
+ // Form consistency: marketplaceForm must agree with sourceDescriptor.form.
1698
+ if (sd.form !== action.marketplaceForm) {
1699
+ return createResult({
1700
+ actionType,
1701
+ status: ActionStatus.PREFLIGHT_FAILED,
1702
+ error: `sourceDescriptor.form "${sd.form}" does not match marketplaceForm "${action.marketplaceForm}"`,
1703
+ });
1704
+ }
1119
1705
 
1120
- // 7. Verify frozen snapshot exists and contains required marketplace files
1121
- const platform = getPlatform(consumer);
1122
- const platformRoute = resolvePlatformRoute(platform);
1123
- let snapshotDirReal;
1124
- // Authoritative kimi manifest (from the frozen snapshot), used to
1125
- // resolve the entry skill via the manifest-declared skills root.
1126
- let kimiSnapshotManifest = null;
1127
- try {
1128
- snapshotDirReal = await resolveFrozenPath(context.root, snapshotPath, 'frozen snapshot path');
1129
- } catch (frozenErr) {
1130
- return createResult({
1131
- actionType,
1132
- status: ActionStatus.PREFLIGHT_FAILED,
1133
- error: `frozen snapshot validation failed: ${frozenErr.message}`,
1134
- });
1706
+ // payloadDigest: must be a valid 64-char lowercase hex string and
1707
+ // must not be the null hash (all zeros).
1708
+ if (typeof sd.payloadDigest !== 'string' || sd.payloadDigest.length === 0) {
1709
+ return createResult({
1710
+ actionType,
1711
+ status: ActionStatus.PREFLIGHT_FAILED,
1712
+ error: 'sourceDescriptor.payloadDigest is required',
1713
+ });
1714
+ }
1715
+ if (!/^[a-f0-9]{64}$/.test(sd.payloadDigest)) {
1716
+ return createResult({
1717
+ actionType,
1718
+ status: ActionStatus.PREFLIGHT_FAILED,
1719
+ error: 'sourceDescriptor.payloadDigest must be a 64-char lowercase hex string',
1720
+ });
1721
+ }
1722
+ if (sd.payloadDigest === '0'.repeat(64)) {
1723
+ return createResult({
1724
+ actionType,
1725
+ status: ActionStatus.PREFLIGHT_FAILED,
1726
+ error: 'sourceDescriptor.payloadDigest must not be the null hash',
1727
+ });
1728
+ }
1729
+
1730
+ // marketplaceEntry: must match the action plugin name.
1731
+ if (typeof sd.marketplaceEntry !== 'string' || sd.marketplaceEntry.length === 0) {
1732
+ return createResult({
1733
+ actionType,
1734
+ status: ActionStatus.PREFLIGHT_FAILED,
1735
+ error: 'sourceDescriptor.marketplaceEntry is required',
1736
+ });
1737
+ }
1738
+ if (sd.marketplaceEntry !== action.plugin) {
1739
+ return createResult({
1740
+ actionType,
1741
+ status: ActionStatus.PREFLIGHT_FAILED,
1742
+ error: `sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${action.plugin}"`,
1743
+ });
1744
+ }
1745
+
1746
+ // Form-specific field validation.
1747
+ if (sd.form === 'bundled-family') {
1748
+ if (!sd.repo || !SAFE_REPO_RE.test(sd.repo)) {
1749
+ return createResult({
1750
+ actionType,
1751
+ status: ActionStatus.PREFLIGHT_FAILED,
1752
+ error: `sourceDescriptor.repo is required and must be a safe repo pattern`,
1753
+ });
1754
+ }
1755
+ if (sd.repo !== action.repo) {
1756
+ return createResult({
1757
+ actionType,
1758
+ status: ActionStatus.PREFLIGHT_FAILED,
1759
+ error: `sourceDescriptor.repo "${sd.repo}" does not match action.repo "${action.repo}"`,
1760
+ });
1761
+ }
1762
+ // commit 可选,但如果存在则必须是 40-hex 格式
1763
+ if (sd.commit !== undefined && (typeof sd.commit !== 'string' || !/^[0-9a-f]{40}$/.test(sd.commit))) {
1764
+ return createResult({
1765
+ actionType,
1766
+ status: ActionStatus.PREFLIGHT_FAILED,
1767
+ error: 'sourceDescriptor.commit must be a 40-hex commit sha for bundled-family form',
1768
+ });
1769
+ }
1770
+ // commit 交叉校验:bundled sourceDescriptor.commit 必须与
1771
+ // action.sourceCommit(冻结的插件来源提交)一致
1772
+ if (sd.commit !== undefined && action.sourceCommit && sd.commit !== action.sourceCommit) {
1773
+ return createResult({
1774
+ actionType,
1775
+ status: ActionStatus.PREFLIGHT_FAILED,
1776
+ error: `sourceDescriptor.commit "${sd.commit}" does not match action.sourceCommit "${action.sourceCommit}"`,
1777
+ });
1778
+ }
1779
+ // payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
1780
+ if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
1781
+ return createResult({
1782
+ actionType,
1783
+ status: ActionStatus.PREFLIGHT_FAILED,
1784
+ error: 'sourceDescriptor.payloadDigest is required for bundled-family form',
1785
+ });
1786
+ }
1787
+ // payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
1788
+ if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
1789
+ return createResult({
1790
+ actionType,
1791
+ status: ActionStatus.PREFLIGHT_FAILED,
1792
+ error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
1793
+ });
1794
+ }
1795
+ if (typeof sd.pluginSubpath !== 'string' || sd.pluginSubpath.length === 0) {
1796
+ return createResult({
1797
+ actionType,
1798
+ status: ActionStatus.PREFLIGHT_FAILED,
1799
+ error: 'sourceDescriptor.pluginSubpath is required for bundled-family form',
1800
+ });
1801
+ }
1802
+ } else if (sd.form === 'standalone-index') {
1803
+ if (!sd.pluginRepo || !SAFE_REPO_RE.test(sd.pluginRepo)) {
1804
+ return createResult({
1805
+ actionType,
1806
+ status: ActionStatus.PREFLIGHT_FAILED,
1807
+ error: `sourceDescriptor.pluginRepo is required for standalone-index form`,
1808
+ });
1809
+ }
1810
+ if (!sd.marketplaceRepo || !SAFE_REPO_RE.test(sd.marketplaceRepo)) {
1811
+ return createResult({
1812
+ actionType,
1813
+ status: ActionStatus.PREFLIGHT_FAILED,
1814
+ error: 'sourceDescriptor.marketplaceRepo is required for standalone-index form',
1815
+ });
1816
+ }
1817
+ // 独立市场身份交叉校验:
1818
+ // 1. marketplaceRepo 必须等于 action.repo(市场仓库身份)
1819
+ if (sd.marketplaceRepo !== action.repo) {
1820
+ return createResult({
1821
+ actionType,
1822
+ status: ActionStatus.PREFLIGHT_FAILED,
1823
+ error: `sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match action.repo "${action.repo}"`,
1824
+ });
1825
+ }
1826
+ // 2. pluginRepo 必须与 marketplaceRepo 不同(发布单元仓库 ≠ 市场仓库)
1827
+ if (sd.pluginRepo === sd.marketplaceRepo) {
1828
+ return createResult({
1829
+ actionType,
1830
+ status: ActionStatus.PREFLIGHT_FAILED,
1831
+ error: `sourceDescriptor.pluginRepo "${sd.pluginRepo}" must differ from marketplaceRepo "${sd.marketplaceRepo}"`,
1832
+ });
1833
+ }
1834
+ if (
1835
+ typeof sd.marketplaceCommitSha !== 'string'
1836
+ || !/^[0-9a-f]{40}$/.test(sd.marketplaceCommitSha)
1837
+ ) {
1838
+ return createResult({
1839
+ actionType,
1840
+ status: ActionStatus.PREFLIGHT_FAILED,
1841
+ error: 'sourceDescriptor.marketplaceCommitSha must be a 40-hex commit sha for standalone-index form',
1842
+ });
1843
+ }
1844
+ // 3. marketplaceCommitSha 交叉校验(与 action 层一致)
1845
+ if (action.marketplaceCommitSha && sd.marketplaceCommitSha !== action.marketplaceCommitSha) {
1846
+ return createResult({
1847
+ actionType,
1848
+ status: ActionStatus.PREFLIGHT_FAILED,
1849
+ error: `sourceDescriptor.marketplaceCommitSha "${sd.marketplaceCommitSha}" does not match action.marketplaceCommitSha "${action.marketplaceCommitSha}"`,
1850
+ });
1851
+ }
1852
+ if (typeof sd.ref !== 'string' || sd.ref.length === 0) {
1853
+ return createResult({
1854
+ actionType,
1855
+ status: ActionStatus.PREFLIGHT_FAILED,
1856
+ error: 'sourceDescriptor.ref is required for standalone-index form',
1857
+ });
1858
+ }
1859
+ // 4. payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
1860
+ if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
1861
+ return createResult({
1862
+ actionType,
1863
+ status: ActionStatus.PREFLIGHT_FAILED,
1864
+ error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for standalone-index form',
1865
+ });
1866
+ }
1867
+ // 5. payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
1868
+ if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
1869
+ return createResult({
1870
+ actionType,
1871
+ status: ActionStatus.PREFLIGHT_FAILED,
1872
+ error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
1873
+ });
1874
+ }
1875
+ } else {
1876
+ return createResult({
1877
+ actionType,
1878
+ status: ActionStatus.PREFLIGHT_FAILED,
1879
+ error: `sourceDescriptor.form "${sd.form}" is not a recognized form (expected "bundled-family" or "standalone-index")`,
1880
+ });
1881
+ }
1882
+ }
1883
+ }
1884
+
1885
+ // 7. Verify frozen snapshot exists and contains required marketplace files
1886
+ const platform = consumerPlatform;
1887
+ const platformRoute = route;
1888
+ let snapshotDirReal;
1889
+ // Authoritative kimi manifest (from the frozen snapshot), used to
1890
+ // resolve the entry skill via the manifest-declared skills root.
1891
+ let kimiSnapshotManifest = null;
1892
+ try {
1893
+ snapshotDirReal = await resolveFrozenPath(context.root, snapshotPath, 'frozen snapshot path');
1894
+ } catch (frozenErr) {
1895
+ return createResult({
1896
+ actionType,
1897
+ status: ActionStatus.PREFLIGHT_FAILED,
1898
+ error: `frozen snapshot validation failed: ${frozenErr.message}`,
1899
+ });
1135
1900
  }
1136
1901
 
1137
1902
  // A human-attestation platform (kimi, codebuddy) has no trustworthy
@@ -1171,7 +1936,13 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1171
1936
  kimiSnapshotManifest = kimiManifest;
1172
1937
  }
1173
1938
 
1939
+ // pluginRootForEntrySkill: the resolved plugin root directory for
1940
+ // entry skill validation. Set inside the structured-cli non-external
1941
+ // path; null for external/human-attestation (falls back to snapshotDirReal).
1942
+ let pluginRootForEntrySkill = null;
1943
+
1174
1944
  if (platformRoute.route === 'structured-cli') {
1945
+ let sourceDirReal = null;
1175
1946
  if (action.marketplaceLocation === 'external') {
1176
1947
  // External independent marketplace form: the marketplace index lives
1177
1948
  // in the external repository (frozen by prepare), NOT in the unit
@@ -1226,9 +1997,21 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1226
1997
  }
1227
1998
  } else {
1228
1999
  // Verify marketplace files exist.
1229
- // marketplace.json is at the snapshot root; plugin manifest is
1230
- // resolved relative to the entry's declared source path.
1231
- const marketplaceRelative = platform.manifestPaths.marketplace;
2000
+ // marketplace.json 的路径:嵌套布局使用 action.marketplaceIndexPath,
2001
+ // 根布局使用平台默认路径。条目 source 相对市场根解析。
2002
+ const marketplaceRelative = action.marketplaceIndexPath ?? platform.manifestPaths.marketplace;
2003
+
2004
+ // 计算市场根(精确后缀匹配)
2005
+ let mktRoot;
2006
+ try {
2007
+ mktRoot = resolveMarketplaceRoot(platform, marketplaceRelative);
2008
+ } catch (mktRootErr) {
2009
+ return createResult({
2010
+ actionType,
2011
+ status: ActionStatus.PREFLIGHT_FAILED,
2012
+ error: `marketplace root resolution failed: ${mktRootErr.message}`,
2013
+ });
2014
+ }
1232
2015
 
1233
2016
  const marketplacePath = resolve(snapshotDirReal, marketplaceRelative);
1234
2017
 
@@ -1276,6 +2059,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1276
2059
  // empty strings. Normalized to "." for root layouts; the same
1277
2060
  // helper backs verify-side payload subtree resolution, so both
1278
2061
  // paths can never drift apart.
2062
+ // source 相对市场根(mktRoot)解析,而非快照根。
1279
2063
  let sourcePath;
1280
2064
  try {
1281
2065
  sourcePath = extractDeclaredPluginSource(consumer, entry);
@@ -1288,8 +2072,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1288
2072
  }
1289
2073
  // Verify the declared source directory exists and contains the
1290
2074
  // expected plugin manifest inside the frozen snapshot.
1291
- const sourceDirAbs = resolve(snapshotDirReal, sourcePath);
1292
- const sourceDirReal = await realpath(sourceDirAbs).catch(() => null);
2075
+ // source 相对市场根解析
2076
+ const mktRootAbs = mktRoot === '.' ? snapshotDirReal : resolve(snapshotDirReal, mktRoot);
2077
+ const sourceDirAbs = resolve(mktRootAbs, sourcePath);
2078
+ sourceDirReal = await realpath(sourceDirAbs).catch(() => null);
1293
2079
  if (!sourceDirReal) {
1294
2080
  return createResult({
1295
2081
  actionType,
@@ -1306,13 +2092,18 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1306
2092
  error: `marketplace plugin entry source "${sourcePath}" escapes the frozen snapshot`,
1307
2093
  });
1308
2094
  }
2095
+ // Save plugin root for entry skill validation (visible outside structured-cli block)
2096
+ pluginRootForEntrySkill = sourceDirReal;
1309
2097
 
1310
2098
  // Resolve plugin manifest relative to the declared source path.
1311
2099
  // For root layouts (source: "./"), this resolves to
1312
2100
  // snapshot/.claude-plugin/plugin.json
1313
- // For subdirectory layouts (source: "./adapters/claude"), this resolves to
1314
- // snapshot/adapters/claude/.claude-plugin/plugin.json
1315
- const manifestRelative = join(sourcePath, platform.manifestPaths.plugin);
2101
+ // For nested layouts (marketRoot: "adapters/claude", source: "."),
2102
+ // this resolves to snapshot/adapters/claude/.claude-plugin/plugin.json
2103
+ const pluginRootRelToSnapshot = relative(snapshotDirReal, sourceDirReal) || '.';
2104
+ const manifestRelative = pluginRootRelToSnapshot === '.'
2105
+ ? platform.manifestPaths.plugin
2106
+ : `${pluginRootRelToSnapshot}/${platform.manifestPaths.plugin}`;
1316
2107
  const manifestPath = resolve(snapshotDirReal, manifestRelative);
1317
2108
 
1318
2109
  const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
@@ -1384,14 +2175,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1384
2175
  });
1385
2176
  }
1386
2177
  } else {
1387
- const entrySkillFile = resolve(snapshotDirReal, 'skills', action.entrySkill, 'SKILL.md');
2178
+ // Entry skill 相对插件根解析(嵌套布局时 pluginRoot != snapshotRoot)。
2179
+ // pluginRootForEntrySkill 在非 external 结构化 CLI 路径中已确定;
2180
+ // external 路径或 human-attestation(null)时回退到 snapshotDirReal。
2181
+ const pluginRootForSkill = pluginRootForEntrySkill || snapshotDirReal;
2182
+ const entrySkillFile = resolve(pluginRootForSkill, 'skills', action.entrySkill, 'SKILL.md');
1388
2183
  try {
1389
2184
  await stat(entrySkillFile);
1390
2185
  } catch {
2186
+ const skillRel = relative(snapshotDirReal, entrySkillFile) || `skills/${action.entrySkill}/SKILL.md`;
1391
2187
  return createResult({
1392
2188
  actionType,
1393
2189
  status: ActionStatus.PREFLIGHT_FAILED,
1394
- error: `entry skill not found in snapshot: skills/${action.entrySkill}/SKILL.md`,
2190
+ error: `entry skill not found in snapshot: ${skillRel}`,
1395
2191
  });
1396
2192
  }
1397
2193
  }
@@ -1414,6 +2210,28 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1414
2210
  });
1415
2211
  }
1416
2212
 
2213
+ // installationContractDigest 摘要校验(新版字段组已通过完整性检查后来到这里)
2214
+ // 从冻结快照读取插件 manifest,按平台注册表和来源形态构建安装契约,
2215
+ // 以 consumer-install-v1 重算并与 action 携带的摘要比对。
2216
+ if (hasNewFieldGroup) {
2217
+ try {
2218
+ const contractResult = await validateInstallationContractDigest(action, snapshotDirReal, consumerPlatform);
2219
+ if (!contractResult.valid) {
2220
+ return createResult({
2221
+ actionType,
2222
+ status: ActionStatus.PREFLIGHT_FAILED,
2223
+ error: contractResult.error,
2224
+ });
2225
+ }
2226
+ } catch (contractErr) {
2227
+ return createResult({
2228
+ actionType,
2229
+ status: ActionStatus.PREFLIGHT_FAILED,
2230
+ error: `installationContractDigest 验证异常: ${contractErr.message}`,
2231
+ });
2232
+ }
2233
+ }
2234
+
1417
2235
  return createResult({
1418
2236
  actionType,
1419
2237
  status: ActionStatus.PREFLIGHT_PASSED,
@@ -1668,6 +2486,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1668
2486
  }
1669
2487
  }
1670
2488
  } catch (addErr) {
2489
+ // Re-throw CLI/transport unavailability errors to outer catch
2490
+ // for human-attestation fallback classification
2491
+ if (isCliOrTransportUnavailable(addErr)) {
2492
+ throw addErr;
2493
+ }
1671
2494
  return createResult({
1672
2495
  actionType,
1673
2496
  status: ActionStatus.EXECUTE_FAILED,
@@ -1742,6 +2565,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1742
2565
  }
1743
2566
  }
1744
2567
  } catch (installErr) {
2568
+ // Re-throw CLI/transport unavailability errors to outer catch
2569
+ // for human-attestation fallback classification
2570
+ if (isCliOrTransportUnavailable(installErr)) {
2571
+ throw installErr;
2572
+ }
1745
2573
  return createResult({
1746
2574
  actionType,
1747
2575
  status: ActionStatus.EXECUTE_FAILED,
@@ -1808,6 +2636,18 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1808
2636
  observation: executeObservation,
1809
2637
  });
1810
2638
  } catch (err) {
2639
+ // Codex human-attestation fallback: ONLY when the CLI interface,
2640
+ // binary, environment, or transport is explicitly unavailable.
2641
+ // Explicit mismatches (identity, version, payload, marketplace entry),
2642
+ // malformed JSON, and program exceptions are hard failures.
2643
+ const failedPlatform = getPlatform(action.consumer);
2644
+ if (
2645
+ failedPlatform.degradationPolicy === 'human-attestation-with-fallback'
2646
+ && failedPlatform.strategy.buildManualRequirement
2647
+ && isCliOrTransportUnavailable(err)
2648
+ ) {
2649
+ return failedPlatform.strategy.buildManualRequirement(action, context);
2650
+ }
1811
2651
  return createResult({
1812
2652
  actionType,
1813
2653
  status: ActionStatus.EXECUTE_FAILED,
@@ -1963,8 +2803,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1963
2803
 
1964
2804
  // Stable, cross-run attestation authority (B). The requirement and
1965
2805
  // attestation live here, keyed by the verified plan digest + plugin,
1966
- // so they survive publish PARTIAL -> reconcile -> verify (each of
1967
- // which uses a fresh runDir).
2806
+ // so they survive across fresh runDirs (publish, verify each use a
2807
+ // new runDir).
1968
2808
  let attestationDir;
1969
2809
  try {
1970
2810
  attestationDir = kimiAuthorityDir(context, boundPlanDigest, action.plugin);
@@ -1992,13 +2832,14 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1992
2832
  },
1993
2833
  });
1994
2834
  }
2835
+ // 完整 requirement 绑定验证:所有冻结动作绑定字段必须一致
1995
2836
  if (
1996
2837
  requirement.planDigest !== boundPlanDigest ||
1997
2838
  requirement.plugin !== action.plugin ||
1998
2839
  requirement.version !== action.version ||
1999
- requirement.entrySkill !== action.entrySkill ||
2000
2840
  requirement.repo !== action.repo ||
2001
- requirement.ref !== expectedRef
2841
+ requirement.ref !== (action.ref ?? `v${action.version}`) ||
2842
+ (action.entrySkill && requirement.entrySkill !== action.entrySkill)
2002
2843
  ) {
2003
2844
  return createResult({
2004
2845
  actionType,
@@ -2039,202 +2880,171 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2039
2880
  });
2040
2881
  }
2041
2882
 
2042
- // The attested install path must live in the documented managed
2043
- // layout under the STABLE, plan-digest-keyed isolated home (B). The
2044
- // managed root is derived from the authority dir (itself derived from
2045
- // the verified plan digest + plugin), so it is identical across
2046
- // publish/reconcile/verify run dirs. No lexical-containment fallback
2047
- // (C): every path must realpath-resolve and stay contained; a missing
2048
- // home/root, a symlink, or an escape fails closed.
2049
- const kimiCodeHome = resolve(attestationDir, 'kimi-home');
2050
- const kimiCodeHomeReal = await realpath(kimiCodeHome).catch(() => null);
2051
- if (!kimiCodeHomeReal) {
2052
- return createResult({
2053
- actionType,
2054
- status: ActionStatus.OBSERVED,
2055
- observation: {
2056
- installed: false,
2057
- error: `KIMI_CODE_HOME does not exist or cannot be resolved: ${kimiCodeHome}`,
2058
- },
2059
- });
2060
- }
2061
- const managedRootReal = await realpath(resolve(kimiCodeHomeReal, KIMI_MANAGED_SUBPATH, action.plugin)).catch(() => null);
2062
- if (!managedRootReal) {
2883
+ // 使用归一化后的 attestation(兼容旧格式)
2884
+ const normalizedAttestation = attestationCheck.normalized;
2885
+ const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
2886
+
2887
+ // 统一人工判定:如果结果是 failed,直接返回失败
2888
+ if (normalizedAttestation.result === 'failed') {
2889
+ const errorMsg = `kimi human result: failed${normalizedAttestation.note ? ` (${normalizedAttestation.note})` : ''}`;
2063
2890
  return createResult({
2064
2891
  actionType,
2065
2892
  status: ActionStatus.OBSERVED,
2893
+ error: errorMsg,
2066
2894
  observation: {
2067
2895
  installed: false,
2068
- error: `kimi managed plugin root does not exist: ${resolve(kimiCodeHomeReal, KIMI_MANAGED_SUBPATH, action.plugin)}`,
2896
+ humanConfirmed: true,
2897
+ result: 'failed',
2898
+ actor: normalizedAttestation.actor,
2899
+ confirmedAt: normalizedAttestation.confirmedAt,
2900
+ error: errorMsg,
2069
2901
  },
2070
2902
  });
2071
2903
  }
2072
- // installPath must be a real directory (not a symlink) that resolves
2073
- // inside the managed root.
2074
- const installPath = resolve(attestation.installPath);
2075
- let installPathStat;
2076
- try {
2077
- installPathStat = await lstat(installPath);
2078
- } catch {
2079
- return createResult({
2080
- actionType,
2081
- status: ActionStatus.OBSERVED,
2082
- observation: { installed: false, error: `kimi install path does not exist: ${attestation.installPath}` },
2083
- });
2084
- }
2085
- if (installPathStat.isSymbolicLink()) {
2086
- return createResult({
2087
- actionType,
2088
- status: ActionStatus.OBSERVED,
2089
- observation: { installed: false, error: `kimi install path must not be a symlink: ${attestation.installPath}` },
2090
- });
2091
- }
2092
- if (!installPathStat.isDirectory()) {
2093
- return createResult({
2094
- actionType,
2095
- status: ActionStatus.OBSERVED,
2096
- observation: { installed: false, error: `kimi install path must be a directory: ${attestation.installPath}` },
2097
- });
2098
- }
2099
- const installPathReal = await realpath(installPath).catch(() => null);
2100
- if (!installPathReal) {
2101
- return createResult({
2102
- actionType,
2103
- status: ActionStatus.OBSERVED,
2104
- observation: { installed: false, error: `kimi install path cannot be resolved: ${attestation.installPath}` },
2105
- });
2106
- }
2107
- const sepK = process.platform === 'win32' ? '\\' : '/';
2108
- const relToManaged = relative(managedRootReal, installPathReal);
2109
- if (
2110
- relToManaged !== '' &&
2111
- (isAbsolute(relToManaged) || relToManaged === '..' || relToManaged.startsWith(`..${sepK}`))
2112
- ) {
2904
+
2905
+ if (requiresInstalledClosure && !normalizedAttestation.installPath) {
2113
2906
  return createResult({
2114
2907
  actionType,
2115
2908
  status: ActionStatus.OBSERVED,
2116
2909
  observation: {
2117
2910
  installed: false,
2118
- error: `kimi install path escapes the managed root (${managedRootReal}): ${attestation.installPath}`,
2911
+ error: 'kimi attestation installPath is required by the skill resource closure gate',
2119
2912
  },
2120
2913
  });
2121
2914
  }
2122
2915
 
2123
- // Read-only payload binding: the installed managed copy must match
2124
- // the sealed frozen authority exactly (transport-normalized).
2125
- let manifestDigest;
2126
- let payloadBinding = null;
2127
- try {
2128
- payloadBinding = await verifyInstalledMarketplacePayload(action, context, installPathReal, consumer);
2129
- manifestDigest = payloadBinding.manifestDigest;
2130
- } catch (digestErr) {
2131
- return createResult({
2132
- actionType,
2133
- status: ActionStatus.OBSERVED,
2134
- observation: {
2135
- installed: true,
2136
- installPath: installPathReal,
2137
- error: `failed to bind installed kimi payload to frozen authority: ${digestErr.message}`,
2138
- },
2139
- });
2140
- }
2141
- if (manifestDigest !== action.manifestDigest) {
2142
- return createResult({
2143
- actionType,
2144
- status: ActionStatus.OBSERVED,
2145
- observation: {
2146
- installed: true,
2147
- installPath: installPathReal,
2148
- error: 'installed kimi payload digest does not match the frozen plan digest',
2149
- },
2150
- });
2151
- }
2916
+ // installPath 验证:必须在 managed root 内,不得逃逸,不得是符号链接
2917
+ let verifiedInstallPath = null;
2918
+ if (normalizedAttestation.installPath) {
2919
+ const managedRoot = resolve(attestationDir, 'kimi-home', 'plugins', 'managed');
2920
+ const installPathAbs = resolve(normalizedAttestation.installPath);
2152
2921
 
2153
- // Entry skill must resolve via the installed manifest's skills root
2154
- // (MAJOR-4), with official manifest precedence.
2155
- let installedManifest;
2156
- let entrySkillFound = false;
2157
- try {
2158
- const readManifest = await platform.strategy.readManifest(installPathReal);
2159
- installedManifest = readManifest.manifest;
2160
- await resolveKimiEntrySkillFile(installPathReal, installedManifest, action.entrySkill);
2161
- entrySkillFound = true;
2162
- } catch (entryErr) {
2163
- return createResult({
2164
- actionType,
2165
- status: ActionStatus.OBSERVED,
2166
- observation: {
2167
- installed: true,
2168
- installPath: installPathReal,
2169
- manifestDigest,
2170
- entrySkillFound: false,
2171
- error: `kimi entry skill not resolvable in installed copy: ${entryErr.message}`,
2172
- },
2173
- });
2174
- }
2922
+ // 先验证 managed root 存在(KIMI_CODE_HOME 检查)
2923
+ const managedRootReal = await realpath(managedRoot).catch(() => null);
2924
+ if (!managedRootReal) {
2925
+ return createResult({
2926
+ actionType,
2927
+ status: ActionStatus.OBSERVED,
2928
+ observation: {
2929
+ installed: false,
2930
+ error: 'KIMI_CODE_HOME does not exist',
2931
+ },
2932
+ });
2933
+ }
2175
2934
 
2176
- // Installed manifest identity must match the frozen action.
2177
- if (installedManifest.name !== action.plugin) {
2178
- return createResult({
2179
- actionType,
2180
- status: ActionStatus.OBSERVED,
2181
- observation: {
2182
- installed: true,
2183
- installPath: installPathReal,
2184
- manifestDigest,
2185
- entrySkillFound: true,
2186
- error: `installed kimi manifest name "${installedManifest.name}" does not match action plugin "${action.plugin}"`,
2187
- },
2188
- });
2935
+ // 检查 installPath 词法路径是否存在(lstat realpath 之前)
2936
+ let lexicalStat;
2937
+ try {
2938
+ lexicalStat = await lstat(installPathAbs);
2939
+ } catch {
2940
+ return createResult({
2941
+ actionType,
2942
+ status: ActionStatus.OBSERVED,
2943
+ observation: {
2944
+ installed: false,
2945
+ error: 'managed plugin root does not exist',
2946
+ },
2947
+ });
2948
+ }
2949
+ if (lexicalStat.isSymbolicLink()) {
2950
+ return createResult({
2951
+ actionType,
2952
+ status: ActionStatus.OBSERVED,
2953
+ observation: {
2954
+ installed: false,
2955
+ error: `kimi attestation installPath must not be a symlink: ${normalizedAttestation.installPath}`,
2956
+ },
2957
+ });
2958
+ }
2959
+
2960
+ const installPathReal = await realpath(installPathAbs).catch(() => null);
2961
+ if (!installPathReal) {
2962
+ return createResult({
2963
+ actionType,
2964
+ status: ActionStatus.OBSERVED,
2965
+ observation: {
2966
+ installed: false,
2967
+ error: `kimi attestation installPath does not exist: ${normalizedAttestation.installPath}`,
2968
+ },
2969
+ });
2970
+ }
2971
+ const rel = relative(managedRootReal, installPathReal);
2972
+ const sep = process.platform === 'win32' ? '\\' : '/';
2973
+ if (rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)) {
2974
+ return createResult({
2975
+ actionType,
2976
+ status: ActionStatus.OBSERVED,
2977
+ observation: {
2978
+ installed: false,
2979
+ error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`,
2980
+ },
2981
+ });
2982
+ }
2983
+ verifiedInstallPath = installPathReal;
2189
2984
  }
2190
- if (installedManifest.version !== action.version) {
2191
- return createResult({
2192
- actionType,
2193
- status: ActionStatus.OBSERVED,
2194
- observation: {
2195
- installed: true,
2196
- installPath: installPathReal,
2197
- manifestDigest,
2198
- entrySkillFound: true,
2199
- error: `installed kimi manifest version "${installedManifest.version}" does not match action version "${action.version}"`,
2200
- },
2201
- });
2985
+
2986
+ // 载荷绑定验证:旧格式含 installPath 时必须绑定冻结载荷并失败关闭。
2987
+ // 身份、版本、载荷、市场来源不一致绝不能被人工 passed 覆盖。
2988
+ let manifestDigest = action.manifestDigest;
2989
+ let payloadBinding = null;
2990
+ if (verifiedInstallPath) {
2991
+ try {
2992
+ payloadBinding = await verifyInstalledMarketplacePayload(
2993
+ action,
2994
+ context,
2995
+ verifiedInstallPath,
2996
+ consumer,
2997
+ );
2998
+ manifestDigest = payloadBinding.manifestDigest;
2999
+ } catch (bindingErr) {
3000
+ return createResult({
3001
+ actionType,
3002
+ status: ActionStatus.OBSERVED,
3003
+ error: `kimi payload binding failed: ${bindingErr.message}`,
3004
+ observation: {
3005
+ installed: false,
3006
+ humanConfirmed: true,
3007
+ result: 'passed',
3008
+ actor: normalizedAttestation.actor,
3009
+ confirmedAt: normalizedAttestation.confirmedAt,
3010
+ error: `kimi payload binding failed: ${bindingErr.message}`,
3011
+ },
3012
+ });
3013
+ }
2202
3014
  }
2203
3015
 
2204
- // Build the observation from independently verified fields only.
2205
- // marketplace is intentionally NOT a kimi identity field (MINOR-1).
2206
- const observation = {
2207
- installed: true,
2208
- installPath: installPathReal,
2209
- entrySkillFound: true,
2210
- entrySkill: action.entrySkill,
2211
- manifestDigest,
2212
- consumer,
2213
- plugin: installedManifest.name,
2214
- version: installedManifest.version,
2215
- repo: action.repo,
2216
- ref: expectedRef,
2217
- ...extraInstalledPathsAudit(payloadBinding),
2218
- };
3016
+ // 统一人工判定:返回人工确认成功
3017
+ // 新格式不含 installPath 时,成功依赖已通过的 preflight 静态冻结校验和人工结果绑定
2219
3018
  return createResult({
2220
3019
  actionType,
2221
3020
  status: ActionStatus.OBSERVED,
2222
- observation,
3021
+ observation: {
3022
+ installed: true,
3023
+ humanConfirmed: true,
3024
+ result: 'passed',
3025
+ actor: normalizedAttestation.actor,
3026
+ confirmedAt: normalizedAttestation.confirmedAt,
3027
+ consumer,
3028
+ plugin: action.plugin,
3029
+ version: action.version,
3030
+ repo: action.repo,
3031
+ ref: expectedRef,
3032
+ entrySkill: action.entrySkill,
3033
+ entrySkillFound: true,
3034
+ manifestDigest,
3035
+ planDigest: boundPlanDigest,
3036
+ ...(verifiedInstallPath ? { installPath: verifiedInstallPath } : {}),
3037
+ ...extraInstalledPathsAudit(payloadBinding),
3038
+ },
2223
3039
  });
2224
3040
  }
2225
3041
 
2226
3042
  // CodeBuddy protocol capability gap (parallel to the kimi branch
2227
- // above; duplicated rather than abstracted so the kimi error strings,
2228
- // file layout, and golden bytes stay byte-for-byte unchanged). The
2229
- // codebuddy CLI cannot pin a frozen ref, so observe never execs a
2230
- // codebuddy command. It consumes a structured human attestation bound
2231
- // to the frozen plan digest + identity, validates the install path per
2232
- // channel (desktop `~/.workbuddy` layout / isolated cli home), then
2233
- // performs read-only verification of the installed copy: payload
2234
- // digest vs the sealed authority, entry skill resolved via the
2235
- // manifest skills root, and manifest name/version. Missing/expired/
2236
- // mismatched/escaping proof fails closed so a codebuddy unit can never
2237
- // reach VERIFIED without it.
3043
+ // above). The codebuddy CLI cannot pin a frozen ref, so observe
3044
+ // never execs a codebuddy command. It consumes a unified human
3045
+ // result bound to the frozen plan digest + identity.
3046
+ // Missing/mismatched proof fails closed so a codebuddy unit can
3047
+ // never reach VERIFIED without it.
2238
3048
  const codebuddyRoute = platform ? resolvePlatformRoute(platform) : null;
2239
3049
  if (platform && codebuddyRoute?.route === 'human-attestation' && actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL) {
2240
3050
  const expectedRef = action.ref ?? `v${action.version}`;
@@ -2256,8 +3066,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2256
3066
  }
2257
3067
 
2258
3068
  // Stable, cross-run attestation authority (B), keyed by the verified
2259
- // plan digest + plugin, so it survives publish PARTIAL -> reconcile
2260
- // -> verify (each uses a fresh runDir).
3069
+ // plan digest + plugin, so it survives across fresh runDirs.
2261
3070
  let attestationDir;
2262
3071
  try {
2263
3072
  attestationDir = codebuddyAuthorityDir(context, boundPlanDigest, action.plugin);
@@ -2285,13 +3094,14 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2285
3094
  },
2286
3095
  });
2287
3096
  }
3097
+ // 完整 requirement 绑定验证:所有冻结动作绑定字段必须一致
2288
3098
  if (
2289
3099
  requirement.planDigest !== boundPlanDigest ||
2290
3100
  requirement.plugin !== action.plugin ||
2291
3101
  requirement.version !== action.version ||
2292
- requirement.entrySkill !== action.entrySkill ||
2293
3102
  requirement.repo !== action.repo ||
2294
- requirement.ref !== expectedRef
3103
+ requirement.ref !== (action.ref ?? `v${action.version}`) ||
3104
+ (action.entrySkill && requirement.entrySkill !== action.entrySkill)
2295
3105
  ) {
2296
3106
  return createResult({
2297
3107
  actionType,
@@ -2332,191 +3142,215 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2332
3142
  });
2333
3143
  }
2334
3144
 
2335
- // The attested install path must be a real directory (not a symlink)
2336
- // that resolves successfully. Per-channel containment:
2337
- // cli: installPath must resolve inside the ISOLATED, plan-digest
2338
- // keyed codebuddy home's marketplace plugin root.
2339
- // desktop: the segment-level `~/.workbuddy/...` layout check already
2340
- // ran in validateCodeBuddyAttestation; here we only require
2341
- // the path to resolve to a real directory.
2342
- const installPath = resolve(attestation.installPath);
2343
- let installPathStat;
2344
- try {
2345
- installPathStat = await lstat(installPath);
2346
- } catch {
2347
- return createResult({
2348
- actionType,
2349
- status: ActionStatus.OBSERVED,
2350
- observation: { installed: false, error: `codebuddy install path does not exist: ${attestation.installPath}` },
2351
- });
2352
- }
2353
- if (installPathStat.isSymbolicLink()) {
2354
- return createResult({
2355
- actionType,
2356
- status: ActionStatus.OBSERVED,
2357
- observation: { installed: false, error: `codebuddy install path must not be a symlink: ${attestation.installPath}` },
2358
- });
2359
- }
2360
- if (!installPathStat.isDirectory()) {
2361
- return createResult({
2362
- actionType,
2363
- status: ActionStatus.OBSERVED,
2364
- observation: { installed: false, error: `codebuddy install path must be a directory: ${attestation.installPath}` },
2365
- });
2366
- }
2367
- const installPathReal = await realpath(installPath).catch(() => null);
2368
- if (!installPathReal) {
3145
+ // 使用归一化后的 attestation(兼容旧格式)
3146
+ const normalizedAttestation = attestationCheck.normalized;
3147
+ const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
3148
+
3149
+ // 统一人工判定:如果结果是 failed,直接返回失败
3150
+ if (normalizedAttestation.result === 'failed') {
3151
+ const errorMsg = `codebuddy human result: failed${normalizedAttestation.note ? ` (${normalizedAttestation.note})` : ''}`;
2369
3152
  return createResult({
2370
3153
  actionType,
2371
3154
  status: ActionStatus.OBSERVED,
2372
- observation: { installed: false, error: `codebuddy install path cannot be resolved: ${attestation.installPath}` },
3155
+ error: errorMsg,
3156
+ observation: {
3157
+ installed: false,
3158
+ humanConfirmed: true,
3159
+ result: 'failed',
3160
+ actor: normalizedAttestation.actor,
3161
+ confirmedAt: normalizedAttestation.confirmedAt,
3162
+ error: errorMsg,
3163
+ },
2373
3164
  });
2374
3165
  }
2375
3166
 
2376
- if (attestation.installChannel === 'cli') {
2377
- const cliHome = codebuddyCliHome(attestationDir);
2378
- const cliHomeReal = await realpath(cliHome).catch(() => null);
2379
- if (!cliHomeReal) {
3167
+ let verifiedInstallPath = null;
3168
+ if (requiresInstalledClosure) {
3169
+ if (!normalizedAttestation.installPath) {
2380
3170
  return createResult({
2381
3171
  actionType,
2382
3172
  status: ActionStatus.OBSERVED,
2383
3173
  observation: {
2384
3174
  installed: false,
2385
- error: `codebuddy isolated CLI home does not exist or cannot be resolved: ${cliHome}`,
3175
+ error: 'codebuddy attestation installPath is required by the skill resource closure gate',
2386
3176
  },
2387
3177
  });
2388
3178
  }
2389
- const cliRootAbs = codebuddyCliInstallRoot(cliHomeReal, attestation.marketplace, action.plugin);
2390
- const cliRootReal = await realpath(cliRootAbs).catch(() => null);
2391
- if (!cliRootReal) {
3179
+ if (!['desktop', 'cli'].includes(normalizedAttestation.installChannel)) {
2392
3180
  return createResult({
2393
3181
  actionType,
2394
3182
  status: ActionStatus.OBSERVED,
2395
3183
  observation: {
2396
3184
  installed: false,
2397
- error: `codebuddy CLI marketplace plugin root does not exist: ${cliRootAbs}`,
3185
+ error: 'codebuddy attestation installChannel must be "desktop" or "cli" when the skill resource closure gate is active',
2398
3186
  },
2399
3187
  });
2400
3188
  }
2401
- const sepC = process.platform === 'win32' ? '\\' : '/';
2402
- const relToRoot = relative(cliRootReal, installPathReal);
2403
- if (
2404
- relToRoot !== '' &&
2405
- (isAbsolute(relToRoot) || relToRoot === '..' || relToRoot.startsWith(`..${sepC}`))
2406
- ) {
3189
+ const normalizedPath = normalizedAttestation.installPath.replaceAll('\\', '/');
3190
+ const expectedSuffix = `.workbuddy/plugins/marketplaces/${CODEBUDDY_MARKETPLACE_NAME}/plugins/${action.plugin}`;
3191
+ if (!normalizedPath.endsWith(expectedSuffix)) {
3192
+ return createResult({
3193
+ actionType,
3194
+ status: ActionStatus.OBSERVED,
3195
+ observation: {
3196
+ installed: false,
3197
+ error: `codebuddy attestation installPath must end with "${expectedSuffix}"`,
3198
+ },
3199
+ });
3200
+ }
3201
+ const installPathAbs = resolve(normalizedAttestation.installPath);
3202
+ let lexicalStat;
3203
+ try {
3204
+ lexicalStat = await lstat(installPathAbs);
3205
+ } catch {
3206
+ return createResult({
3207
+ actionType,
3208
+ status: ActionStatus.OBSERVED,
3209
+ observation: {
3210
+ installed: false,
3211
+ error: `codebuddy attestation installPath does not exist: ${normalizedAttestation.installPath}`,
3212
+ },
3213
+ });
3214
+ }
3215
+ if (lexicalStat.isSymbolicLink() || !lexicalStat.isDirectory()) {
2407
3216
  return createResult({
2408
3217
  actionType,
2409
3218
  status: ActionStatus.OBSERVED,
2410
3219
  observation: {
2411
3220
  installed: false,
2412
- error: `codebuddy install path escapes the isolated CLI marketplace root (${cliRootReal}): ${attestation.installPath}`,
3221
+ error: 'codebuddy attestation installPath must be a real directory, not a symlink',
2413
3222
  },
2414
3223
  });
2415
3224
  }
3225
+ verifiedInstallPath = await realpath(installPathAbs);
2416
3226
  }
2417
3227
 
2418
- // Read-only payload binding: the installed copy must match the
2419
- // sealed frozen authority exactly (transport-normalized).
2420
- let manifestDigest;
2421
- let payloadBinding = null;
2422
- try {
2423
- payloadBinding = await verifyInstalledMarketplacePayload(action, context, installPathReal, consumer);
2424
- manifestDigest = payloadBinding.manifestDigest;
2425
- } catch (digestErr) {
2426
- return createResult({
2427
- actionType,
2428
- status: ActionStatus.OBSERVED,
2429
- observation: {
2430
- installed: true,
2431
- installPath: installPathReal,
2432
- error: `failed to bind installed codebuddy payload to frozen authority: ${digestErr.message}`,
2433
- },
2434
- });
2435
- }
2436
- if (manifestDigest !== action.manifestDigest) {
2437
- return createResult({
2438
- actionType,
2439
- status: ActionStatus.OBSERVED,
2440
- observation: {
2441
- installed: true,
2442
- installPath: installPathReal,
2443
- error: 'installed codebuddy payload digest does not match the frozen plan digest',
2444
- },
2445
- });
2446
- }
3228
+ // 人工结果仍裁决不可自动化的安装步骤;新资源闭包计划另外
3229
+ // 暴露已校验的真实安装目录,供 verify 的内置只读门禁扫描。
3230
+ return createResult({
3231
+ actionType,
3232
+ status: ActionStatus.OBSERVED,
3233
+ observation: {
3234
+ installed: true,
3235
+ humanConfirmed: true,
3236
+ result: 'passed',
3237
+ actor: normalizedAttestation.actor,
3238
+ confirmedAt: normalizedAttestation.confirmedAt,
3239
+ consumer,
3240
+ plugin: action.plugin,
3241
+ version: action.version,
3242
+ repo: action.repo,
3243
+ ref: expectedRef,
3244
+ entrySkill: action.entrySkill,
3245
+ entrySkillFound: true,
3246
+ manifestDigest: action.manifestDigest,
3247
+ planDigest: boundPlanDigest,
3248
+ ...(verifiedInstallPath ? { installPath: verifiedInstallPath } : {}),
3249
+ },
3250
+ });
3251
+ }
3252
+
3253
+ // Codex human-attestation fallback: if the CLI was unavailable
3254
+ // during execute, a human attestation may exist in the authority dir.
3255
+ // Read and validate it instead of the CLI evidence.
3256
+ //
3257
+ // CRITICAL: observe only reads human results when a bound, validated
3258
+ // Codex manual requirement exists. Isolated attestation files must not
3259
+ // bypass automatic evidence.
3260
+ if (
3261
+ platform.degradationPolicy === 'human-attestation-with-fallback'
3262
+ && actionType === ActionType.CODEX_MARKETPLACE_INSTALL
3263
+ ) {
3264
+ const expectedRef = action.ref ?? `v${action.version}`;
2447
3265
 
2448
- // Entry skill must resolve via the installed manifest's skills root,
2449
- // with the codebuddy single-candidate manifest.
2450
- let installedManifest;
3266
+ // Try to read the human attestation from the authority dir.
3267
+ let codexBoundPlanDigest;
2451
3268
  try {
2452
- const readManifest = await platform.strategy.readManifest(installPathReal);
2453
- installedManifest = readManifest.manifest;
2454
- await resolveCodeBuddyEntrySkillFile(installPathReal, installedManifest, action.entrySkill);
2455
- } catch (entryErr) {
2456
- return createResult({
2457
- actionType,
2458
- status: ActionStatus.OBSERVED,
2459
- observation: {
2460
- installed: true,
2461
- installPath: installPathReal,
2462
- manifestDigest,
2463
- entrySkillFound: false,
2464
- error: `codebuddy entry skill not resolvable in installed copy: ${entryErr.message}`,
2465
- },
2466
- });
3269
+ codexBoundPlanDigest = await resolveCodexBoundPlanDigest(context);
3270
+ } catch {
3271
+ // No frozen plan — cannot read attestation
2467
3272
  }
3273
+ if (codexBoundPlanDigest) {
3274
+ let codexAttestationDir;
3275
+ try {
3276
+ codexAttestationDir = codexAuthorityDir(context, codexBoundPlanDigest, action.plugin);
3277
+ } catch {
3278
+ // Authority dir not resolvable
3279
+ }
3280
+ if (codexAttestationDir) {
3281
+ // CRITICAL: Only read attestation if requirement exists and is bound
3282
+ let codexRequirement = null;
3283
+ try {
3284
+ codexRequirement = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_REQUIREMENT_FILE), 'utf8'));
3285
+ } catch {
3286
+ // No requirement file — must not read isolated attestation
3287
+ }
2468
3288
 
2469
- // Installed manifest identity must match the frozen action.
2470
- if (installedManifest.name !== action.plugin) {
2471
- return createResult({
2472
- actionType,
2473
- status: ActionStatus.OBSERVED,
2474
- observation: {
2475
- installed: true,
2476
- installPath: installPathReal,
2477
- manifestDigest,
2478
- entrySkillFound: true,
2479
- error: `installed codebuddy manifest name "${installedManifest.name}" does not match action plugin "${action.plugin}"`,
2480
- },
2481
- });
2482
- }
2483
- if (installedManifest.version !== action.version) {
2484
- return createResult({
2485
- actionType,
2486
- status: ActionStatus.OBSERVED,
2487
- observation: {
2488
- installed: true,
2489
- installPath: installPathReal,
2490
- manifestDigest,
2491
- entrySkillFound: true,
2492
- error: `installed codebuddy manifest version "${installedManifest.version}" does not match action version "${action.version}"`,
2493
- },
2494
- });
3289
+ // Validate requirement binds to this action and plan (完整绑定验证)
3290
+ if (
3291
+ codexRequirement
3292
+ && codexRequirement.planDigest === codexBoundPlanDigest
3293
+ && codexRequirement.plugin === action.plugin
3294
+ && codexRequirement.version === action.version
3295
+ && codexRequirement.repo === action.repo
3296
+ && codexRequirement.ref === (action.ref ?? `v${action.version}`)
3297
+ && (!action.entrySkill || codexRequirement.entrySkill === action.entrySkill)
3298
+ ) {
3299
+ let codexAttestation = null;
3300
+ try {
3301
+ codexAttestation = JSON.parse(await readFile(resolve(codexAttestationDir, CODEX_ATTESTATION_FILE), 'utf8'));
3302
+ } catch {
3303
+ // No attestation file — fall through to normal CLI evidence path
3304
+ }
3305
+ if (codexAttestation) {
3306
+ const codexCheck = validateCodexAttestation(codexAttestation, action, new Date().toISOString(), codexBoundPlanDigest);
3307
+ if (!codexCheck.valid) {
3308
+ return createResult({
3309
+ actionType,
3310
+ status: ActionStatus.OBSERVED,
3311
+ observation: { installed: false, error: codexCheck.error },
3312
+ });
3313
+ }
3314
+ if (codexAttestation.result === 'failed') {
3315
+ const errorMsg = `codex human result: failed${codexAttestation.note ? ` (${codexAttestation.note})` : ''}`;
3316
+ return createResult({
3317
+ actionType,
3318
+ status: ActionStatus.OBSERVED,
3319
+ error: errorMsg,
3320
+ observation: {
3321
+ installed: false,
3322
+ humanConfirmed: true,
3323
+ result: 'failed',
3324
+ actor: codexAttestation.actor,
3325
+ confirmedAt: codexAttestation.confirmedAt,
3326
+ error: errorMsg,
3327
+ },
3328
+ });
3329
+ }
3330
+ return createResult({
3331
+ actionType,
3332
+ status: ActionStatus.OBSERVED,
3333
+ observation: {
3334
+ installed: true,
3335
+ humanConfirmed: true,
3336
+ result: 'passed',
3337
+ actor: codexAttestation.actor,
3338
+ confirmedAt: codexAttestation.confirmedAt,
3339
+ consumer,
3340
+ plugin: action.plugin,
3341
+ version: action.version,
3342
+ repo: action.repo,
3343
+ ref: expectedRef,
3344
+ entrySkill: action.entrySkill,
3345
+ entrySkillFound: true,
3346
+ manifestDigest: action.manifestDigest,
3347
+ planDigest: codexBoundPlanDigest,
3348
+ },
3349
+ });
3350
+ }
3351
+ }
3352
+ }
2495
3353
  }
2496
-
2497
- // Build the observation from independently verified fields only.
2498
- // marketplace + installChannel ARE validated codebuddy identity
2499
- // fields (unlike kimi's MINOR-1 marketplace exclusion).
2500
- const observation = {
2501
- installed: true,
2502
- installPath: installPathReal,
2503
- entrySkillFound: true,
2504
- entrySkill: action.entrySkill,
2505
- manifestDigest,
2506
- consumer,
2507
- plugin: installedManifest.name,
2508
- version: installedManifest.version,
2509
- repo: action.repo,
2510
- ref: expectedRef,
2511
- marketplace: attestation.marketplace,
2512
- installChannel: attestation.installChannel,
2513
- ...extraInstalledPathsAudit(payloadBinding),
2514
- };
2515
- return createResult({
2516
- actionType,
2517
- status: ActionStatus.OBSERVED,
2518
- observation,
2519
- });
2520
3354
  }
2521
3355
 
2522
3356
  // Read execute evidence — mandatory for observe validation