release-skill 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-skill",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Safe preparation and frozen GitHub/npm production publishing with full happy end verification",
5
5
  "author": {
6
6
  "name": "广州市风荷科技有限公司"
@@ -31,7 +31,7 @@ import {
31
31
 
32
32
  import { createHash } from 'node:crypto';
33
33
  import { computeFrozenSnapshot, resolveFrozenPath } from '../snapshot/frozen.mjs';
34
- import { PLATFORMS, getPlatform } from '../platforms/registry.mjs';
34
+ import { PLATFORMS, getPlatform, resolvePlatformRoute, resolveCapabilityConflicts } from '../platforms/registry.mjs';
35
35
  import {
36
36
  KIMI_REQUIREMENT_FILE,
37
37
  KIMI_ATTESTATION_FILE,
@@ -448,21 +448,37 @@ async function resolveKimiEntrySkillFile(pluginRootReal, manifest, entrySkill) {
448
448
  * @returns {string} normalized relative path ('' for the plugin root itself)
449
449
  */
450
450
  function normalizeCodeBuddySkillsRel(skillsRaw) {
451
- if (typeof skillsRaw !== 'string' || skillsRaw.length === 0) {
451
+ // CodeBuddy real validator accepts array form; extract first element for
452
+ // backward compatibility with string form.
453
+ let skillsPath;
454
+ if (Array.isArray(skillsRaw)) {
455
+ if (skillsRaw.length !== 1) {
456
+ throw new Error(
457
+ `codebuddy manifest skills array must have exactly one element, got ${skillsRaw.length}`,
458
+ );
459
+ }
460
+ skillsPath = skillsRaw[0];
461
+ } else if (typeof skillsRaw === 'string') {
462
+ skillsPath = skillsRaw;
463
+ } else {
464
+ throw new Error('codebuddy manifest skills must be a string or single-element array when present');
465
+ }
466
+
467
+ if (typeof skillsPath !== 'string' || skillsPath.length === 0) {
452
468
  throw new Error('codebuddy manifest skills must be a non-empty relative path when present');
453
469
  }
454
470
  if (
455
- skillsRaw.startsWith('/') ||
456
- skillsRaw.includes('..') ||
457
- skillsRaw.includes('\\') ||
458
- /^https?:\/\//i.test(skillsRaw)
471
+ skillsPath.startsWith('/') ||
472
+ skillsPath.includes('..') ||
473
+ skillsPath.includes('\\') ||
474
+ /^https?:\/\//i.test(skillsPath)
459
475
  ) {
460
- throw new Error(`codebuddy manifest skills "${skillsRaw}" is not a safe relative path`);
476
+ throw new Error(`codebuddy manifest skills "${skillsPath}" is not a safe relative path`);
461
477
  }
462
- let rel = skillsRaw.replace(/^\.\//, '');
478
+ let rel = skillsPath.replace(/^\.\//, '');
463
479
  rel = rel.replace(/\/+$/, '');
464
480
  if (rel.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
465
- throw new Error(`codebuddy manifest skills "${skillsRaw}" is not a safe relative path`);
481
+ throw new Error(`codebuddy manifest skills "${skillsPath}" is not a safe relative path`);
466
482
  }
467
483
  return rel;
468
484
  }
@@ -551,6 +567,9 @@ const SUPPORTED_TYPES = [
551
567
  /** Safe repo pattern: owner/repo with alphanumeric, hyphens, dots, underscores. */
552
568
  const SAFE_REPO_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
553
569
 
570
+ /** Safe digest pattern: 64-hex SHA-256. */
571
+ const SAFE_DIGEST_RE = /^[0-9a-f]{64}$/;
572
+
554
573
  /** Valid consumer ids, derived from the platform registry (single source). */
555
574
  const CONSUMER_IDS = new Set(PLATFORMS.map((p) => p.id));
556
575
 
@@ -634,7 +653,9 @@ function validateMarketplaceParams(params) {
634
653
  // marketplace but NO non-interactive install API, so `marketplace` carries no
635
654
  // executable meaning for kimi and is optional (validated only if present); it
636
655
  // must not become a required identity condition for kimi execution/observe.
637
- if (!getPlatform(consumer).automatable) {
656
+ const consumerPlatform = getPlatform(consumer);
657
+ const consumerRoute = resolvePlatformRoute(consumerPlatform);
658
+ if (consumerRoute.route === 'human-attestation') {
638
659
  if (marketplace !== undefined && marketplace !== null && !SAFE_ID_RE.test(marketplace)) {
639
660
  return { valid: false, error: `unsafe marketplace identifier: "${marketplace}"` };
640
661
  }
@@ -881,9 +902,224 @@ export function createPluginMarketplaceAdapter(deps = {}) {
881
902
  });
882
903
  }
883
904
 
884
- // 6. Verify frozen snapshot exists and contains required marketplace files
905
+ // Capability conflict check (before sourceDescriptor validation).
906
+ // If the platform definition is internally inconsistent, fail closed.
885
907
  const consumer = action.consumer;
908
+ const consumerPlatform = getPlatform(consumer);
909
+ const capabilityConflicts = resolveCapabilityConflicts(consumerPlatform);
910
+ if (capabilityConflicts.length > 0) {
911
+ return createResult({
912
+ actionType,
913
+ status: ActionStatus.PREFLIGHT_FAILED,
914
+ error: `platform capability conflict: ${capabilityConflicts.join('; ')}`,
915
+ });
916
+ }
917
+
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.
922
+ const route = resolvePlatformRoute(consumerPlatform);
923
+ if (route.route === 'structured-cli') {
924
+ const sd = action.sourceDescriptor;
925
+ if (!sd || typeof sd !== 'object') {
926
+ return createResult({
927
+ actionType,
928
+ status: ActionStatus.PREFLIGHT_FAILED,
929
+ error: 'sourceDescriptor is required for marketplace install',
930
+ });
931
+ }
932
+
933
+ // Form consistency: marketplaceForm must agree with sourceDescriptor.form.
934
+ if (sd.form !== action.marketplaceForm) {
935
+ return createResult({
936
+ actionType,
937
+ status: ActionStatus.PREFLIGHT_FAILED,
938
+ error: `sourceDescriptor.form "${sd.form}" does not match marketplaceForm "${action.marketplaceForm}"`,
939
+ });
940
+ }
941
+
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) {
945
+ return createResult({
946
+ actionType,
947
+ status: ActionStatus.PREFLIGHT_FAILED,
948
+ error: 'sourceDescriptor.payloadDigest is required',
949
+ });
950
+ }
951
+ if (!/^[a-f0-9]{64}$/.test(sd.payloadDigest)) {
952
+ return createResult({
953
+ actionType,
954
+ status: ActionStatus.PREFLIGHT_FAILED,
955
+ error: 'sourceDescriptor.payloadDigest must be a 64-char lowercase hex string',
956
+ });
957
+ }
958
+ if (sd.payloadDigest === '0'.repeat(64)) {
959
+ return createResult({
960
+ actionType,
961
+ status: ActionStatus.PREFLIGHT_FAILED,
962
+ error: 'sourceDescriptor.payloadDigest must not be the null hash',
963
+ });
964
+ }
965
+
966
+ // marketplaceEntry: must match the action plugin name.
967
+ if (typeof sd.marketplaceEntry !== 'string' || sd.marketplaceEntry.length === 0) {
968
+ return createResult({
969
+ actionType,
970
+ status: ActionStatus.PREFLIGHT_FAILED,
971
+ error: 'sourceDescriptor.marketplaceEntry is required',
972
+ });
973
+ }
974
+ if (sd.marketplaceEntry !== action.plugin) {
975
+ return createResult({
976
+ actionType,
977
+ status: ActionStatus.PREFLIGHT_FAILED,
978
+ error: `sourceDescriptor.marketplaceEntry "${sd.marketplaceEntry}" does not match action plugin "${action.plugin}"`,
979
+ });
980
+ }
981
+
982
+ // Form-specific field validation.
983
+ if (sd.form === 'bundled-family') {
984
+ if (!sd.repo || !SAFE_REPO_RE.test(sd.repo)) {
985
+ return createResult({
986
+ actionType,
987
+ status: ActionStatus.PREFLIGHT_FAILED,
988
+ error: `sourceDescriptor.repo is required and must be a safe repo pattern`,
989
+ });
990
+ }
991
+ if (sd.repo !== action.repo) {
992
+ return createResult({
993
+ actionType,
994
+ status: ActionStatus.PREFLIGHT_FAILED,
995
+ error: `sourceDescriptor.repo "${sd.repo}" does not match action.repo "${action.repo}"`,
996
+ });
997
+ }
998
+ // commit 可选,但如果存在则必须是 40-hex 格式
999
+ if (sd.commit !== undefined && (typeof sd.commit !== 'string' || !/^[0-9a-f]{40}$/.test(sd.commit))) {
1000
+ return createResult({
1001
+ actionType,
1002
+ status: ActionStatus.PREFLIGHT_FAILED,
1003
+ error: 'sourceDescriptor.commit must be a 40-hex commit sha for bundled-family form',
1004
+ });
1005
+ }
1006
+ // commit 交叉校验:bundled sourceDescriptor.commit 必须与
1007
+ // action.sourceCommit(冻结的插件来源提交)一致
1008
+ if (sd.commit !== undefined && action.sourceCommit && sd.commit !== action.sourceCommit) {
1009
+ return createResult({
1010
+ actionType,
1011
+ status: ActionStatus.PREFLIGHT_FAILED,
1012
+ error: `sourceDescriptor.commit "${sd.commit}" does not match action.sourceCommit "${action.sourceCommit}"`,
1013
+ });
1014
+ }
1015
+ // payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
1016
+ if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
1017
+ return createResult({
1018
+ actionType,
1019
+ status: ActionStatus.PREFLIGHT_FAILED,
1020
+ error: 'sourceDescriptor.payloadDigest is required for bundled-family form',
1021
+ });
1022
+ }
1023
+ // payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
1024
+ if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
1025
+ return createResult({
1026
+ actionType,
1027
+ status: ActionStatus.PREFLIGHT_FAILED,
1028
+ error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
1029
+ });
1030
+ }
1031
+ if (typeof sd.pluginSubpath !== 'string' || sd.pluginSubpath.length === 0) {
1032
+ return createResult({
1033
+ actionType,
1034
+ status: ActionStatus.PREFLIGHT_FAILED,
1035
+ error: 'sourceDescriptor.pluginSubpath is required for bundled-family form',
1036
+ });
1037
+ }
1038
+ } else if (sd.form === 'standalone-index') {
1039
+ if (!sd.pluginRepo || !SAFE_REPO_RE.test(sd.pluginRepo)) {
1040
+ return createResult({
1041
+ actionType,
1042
+ status: ActionStatus.PREFLIGHT_FAILED,
1043
+ error: `sourceDescriptor.pluginRepo is required for standalone-index form`,
1044
+ });
1045
+ }
1046
+ if (!sd.marketplaceRepo || !SAFE_REPO_RE.test(sd.marketplaceRepo)) {
1047
+ return createResult({
1048
+ actionType,
1049
+ status: ActionStatus.PREFLIGHT_FAILED,
1050
+ error: 'sourceDescriptor.marketplaceRepo is required for standalone-index form',
1051
+ });
1052
+ }
1053
+ // 独立市场身份交叉校验:
1054
+ // 1. marketplaceRepo 必须等于 action.repo(市场仓库身份)
1055
+ if (sd.marketplaceRepo !== action.repo) {
1056
+ return createResult({
1057
+ actionType,
1058
+ status: ActionStatus.PREFLIGHT_FAILED,
1059
+ error: `sourceDescriptor.marketplaceRepo "${sd.marketplaceRepo}" does not match action.repo "${action.repo}"`,
1060
+ });
1061
+ }
1062
+ // 2. pluginRepo 必须与 marketplaceRepo 不同(发布单元仓库 ≠ 市场仓库)
1063
+ if (sd.pluginRepo === sd.marketplaceRepo) {
1064
+ return createResult({
1065
+ actionType,
1066
+ status: ActionStatus.PREFLIGHT_FAILED,
1067
+ error: `sourceDescriptor.pluginRepo "${sd.pluginRepo}" must differ from marketplaceRepo "${sd.marketplaceRepo}"`,
1068
+ });
1069
+ }
1070
+ if (
1071
+ typeof sd.marketplaceCommitSha !== 'string'
1072
+ || !/^[0-9a-f]{40}$/.test(sd.marketplaceCommitSha)
1073
+ ) {
1074
+ return createResult({
1075
+ actionType,
1076
+ status: ActionStatus.PREFLIGHT_FAILED,
1077
+ error: 'sourceDescriptor.marketplaceCommitSha must be a 40-hex commit sha for standalone-index form',
1078
+ });
1079
+ }
1080
+ // 3. marketplaceCommitSha 交叉校验(与 action 层一致)
1081
+ if (action.marketplaceCommitSha && sd.marketplaceCommitSha !== action.marketplaceCommitSha) {
1082
+ return createResult({
1083
+ actionType,
1084
+ status: ActionStatus.PREFLIGHT_FAILED,
1085
+ error: `sourceDescriptor.marketplaceCommitSha "${sd.marketplaceCommitSha}" does not match action.marketplaceCommitSha "${action.marketplaceCommitSha}"`,
1086
+ });
1087
+ }
1088
+ if (typeof sd.ref !== 'string' || sd.ref.length === 0) {
1089
+ return createResult({
1090
+ actionType,
1091
+ status: ActionStatus.PREFLIGHT_FAILED,
1092
+ error: 'sourceDescriptor.ref is required for standalone-index form',
1093
+ });
1094
+ }
1095
+ // 4. payloadDigest 格式校验(可选,但如果存在则必须是 64-hex)
1096
+ if (sd.payloadDigest !== undefined && (!sd.payloadDigest || !SAFE_DIGEST_RE.test(sd.payloadDigest))) {
1097
+ return createResult({
1098
+ actionType,
1099
+ status: ActionStatus.PREFLIGHT_FAILED,
1100
+ error: 'sourceDescriptor.payloadDigest must be a 64-hex digest for standalone-index form',
1101
+ });
1102
+ }
1103
+ // 5. payloadDigest 交叉校验:必须与 action.manifestDigest(冻结的载荷摘要)一致
1104
+ if (sd.payloadDigest !== undefined && action.manifestDigest && sd.payloadDigest !== action.manifestDigest) {
1105
+ return createResult({
1106
+ actionType,
1107
+ status: ActionStatus.PREFLIGHT_FAILED,
1108
+ error: `sourceDescriptor.payloadDigest does not match action.manifestDigest`,
1109
+ });
1110
+ }
1111
+ } else {
1112
+ return createResult({
1113
+ actionType,
1114
+ status: ActionStatus.PREFLIGHT_FAILED,
1115
+ error: `sourceDescriptor.form "${sd.form}" is not a recognized form (expected "bundled-family" or "standalone-index")`,
1116
+ });
1117
+ }
1118
+ }
1119
+
1120
+ // 7. Verify frozen snapshot exists and contains required marketplace files
886
1121
  const platform = getPlatform(consumer);
1122
+ const platformRoute = resolvePlatformRoute(platform);
887
1123
  let snapshotDirReal;
888
1124
  // Authoritative kimi manifest (from the frozen snapshot), used to
889
1125
  // resolve the entry skill via the manifest-declared skills root.
@@ -898,12 +1134,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
898
1134
  });
899
1135
  }
900
1136
 
901
- // A non-automatable platform (kimi, codebuddy) has no trustworthy
1137
+ // A human-attestation platform (kimi, codebuddy) has no trustworthy
902
1138
  // automated install: the whole repo is installed as one plugin. The
903
1139
  // authoritative manifest is read from the verified snapshot root via
904
1140
  // the platform strategy (kimi: kimi.plugin.json over
905
1141
  // .kimi-plugin/plugin.json; codebuddy: .codebuddy-plugin/plugin.json).
906
- if (!platform.automatable) {
1142
+ if (platformRoute.route === 'human-attestation') {
907
1143
  // Error-message label keeps the kimi wording byte-identical while
908
1144
  // giving codebuddy its own wording.
909
1145
  const manifestLabel = consumer === 'codebuddy' ? 'codebuddy' : 'kimi';
@@ -935,7 +1171,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
935
1171
  kimiSnapshotManifest = kimiManifest;
936
1172
  }
937
1173
 
938
- if (platform.automatable) {
1174
+ if (platformRoute.route === 'structured-cli') {
939
1175
  if (action.marketplaceLocation === 'external') {
940
1176
  // External independent marketplace form: the marketplace index lives
941
1177
  // in the external repository (frozen by prepare), NOT in the unit
@@ -1128,13 +1364,13 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1128
1364
  }
1129
1365
 
1130
1366
  // Verify the entry skill exists in the snapshot.
1131
- // Automatable platforms' manifests always declare ./skills/, so the
1367
+ // Structured-cli platforms' manifests always declare ./skills/, so the
1132
1368
  // fixed skills/<entrySkill>/SKILL.md layout is authoritative for
1133
- // them. A non-automatable platform (kimi, codebuddy) resolves the
1369
+ // them. A human-attestation platform (kimi, codebuddy) resolves the
1134
1370
  // entry skill via the manifest-declared skills root (MAJOR-4): the
1135
1371
  // root is validated + realpath-contained, and omitted `skills` means
1136
1372
  // the official single-skill root SKILL.md.
1137
- if (!platform.automatable) {
1373
+ if (platformRoute.route === 'human-attestation') {
1138
1374
  const resolveEntrySkillFile = consumer === 'codebuddy'
1139
1375
  ? resolveCodeBuddyEntrySkillFile
1140
1376
  : resolveKimiEntrySkillFile;
@@ -1341,10 +1577,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1341
1577
  // It is handled entirely by its manual-requirement strategy, which
1342
1578
  // uses a stable plan-digest-keyed home and deliberately SKIPS the
1343
1579
  // per-run isolated consumer dir and its runDir containment check
1344
- // (that model only fits automatable platforms, which exec a real
1580
+ // (that model only fits structured-cli platforms, which exec a real
1345
1581
  // CLI into a per-run HOME).
1346
1582
  const platform = getPlatform(action.consumer);
1347
- if (!platform.automatable) {
1583
+ const executeRoute = resolvePlatformRoute(platform);
1584
+ if (executeRoute.route === 'human-attestation') {
1348
1585
  return platform.strategy.buildManualRequirement(action, context);
1349
1586
  }
1350
1587
 
@@ -1662,19 +1899,22 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1662
1899
  });
1663
1900
  }
1664
1901
  const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
1665
- // Registry-driven platform data. observe historically applies no
1666
- // consumer validation gate (execute/preflight validate it), so an
1667
- // unregistered consumer keeps the legacy fall-through shape: a
1668
- // kimi-shaped env, no attestation branch, and no CLI binary — it
1669
- // still fails closed on the missing execute evidence below.
1902
+ // Registry-driven platform data. An unregistered consumer is a hard
1903
+ // error: the platform registry is the single source of truth for
1904
+ // consumer-platform knowledge, and silently falling through to a
1905
+ // kimi-shaped env would mask configuration mistakes.
1670
1906
  const platform = PLATFORMS.find((p) => p.id === consumer) ?? null;
1671
- const cliCmd = platform ? (platform.cli ? platform.cli.binary : null) : 'kimi';
1907
+ if (!platform) {
1908
+ throw new Error(
1909
+ `Unknown consumer platform "${consumer}" for action "${actionType}". `
1910
+ + `Registered platforms: ${PLATFORMS.map((p) => p.id).join(', ')}`,
1911
+ );
1912
+ }
1913
+ const cliCmd = platform.cli ? platform.cli.binary : null;
1672
1914
  const baseEnv = { ...process.env, ...(context.env ?? {}) };
1673
1915
  const env = {
1674
1916
  ...baseEnv,
1675
- ...(platform
1676
- ? platform.isolationEnv(isolatedHome)
1677
- : { HOME: isolatedHome, KIMI_CODE_HOME: isolatedHome }),
1917
+ ...platform.isolationEnv(isolatedHome),
1678
1918
  };
1679
1919
 
1680
1920
  // Resolve frozen timeoutMs from the expanded action (top-level).
@@ -1701,7 +1941,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1701
1941
  // root (MAJOR-4), and manifest name/version.
1702
1942
  // Missing/expired/mismatched/escaping proof fails closed so a kimi
1703
1943
  // unit can never reach VERIFIED without it.
1704
- if (platform && !platform.automatable && actionType === ActionType.KIMI_MARKETPLACE_INSTALL) {
1944
+ const kimiRoute = platform ? resolvePlatformRoute(platform) : null;
1945
+ if (platform && kimiRoute?.route === 'human-attestation' && actionType === ActionType.KIMI_MARKETPLACE_INSTALL) {
1705
1946
  const expectedRef = action.ref ?? `v${action.version}`;
1706
1947
 
1707
1948
  // Bind to the REAL frozen plan digest (A). Fail closed if the
@@ -1994,7 +2235,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1994
2235
  // manifest skills root, and manifest name/version. Missing/expired/
1995
2236
  // mismatched/escaping proof fails closed so a codebuddy unit can never
1996
2237
  // reach VERIFIED without it.
1997
- if (platform && !platform.automatable && actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL) {
2238
+ const codebuddyRoute = platform ? resolvePlatformRoute(platform) : null;
2239
+ if (platform && codebuddyRoute?.route === 'human-attestation' && actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL) {
1998
2240
  const expectedRef = action.ref ?? `v${action.version}`;
1999
2241
 
2000
2242
  // Bind to the REAL frozen plan digest (A). Fail closed if the
@@ -1258,6 +1258,39 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1258
1258
  // but no frozen ref/marketplaceCommitSha (production-only bindings),
1259
1259
  // keeping the two loops' shapes aligned for plan completeness.
1260
1260
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1261
+ // Normalized marketplace form: explicit mutually exclusive declaration.
1262
+ // bundled-family: marketplace and plugin live in the same repo.
1263
+ // standalone-index: external marketplace repo indexes a separate plugin repo.
1264
+ // Non-automatable platforms (kimi, codebuddy) use human-attestation and
1265
+ // carry no marketplace form.
1266
+ const marketplaceForm = requiresMarketplace
1267
+ ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1268
+ : null;
1269
+ const sourceDescriptor = marketplaceForm === 'standalone-index'
1270
+ ? Object.freeze({
1271
+ form: 'standalone-index',
1272
+ marketplaceRepo: dist.marketplaceRepo,
1273
+ marketplaceEntry: dist.plugin,
1274
+ // pluginRepo is the plugin's own public repo, NOT the external
1275
+ // marketplace repo. The marketplace repo contains the index; the
1276
+ // plugin repo contains the actual plugin code.
1277
+ pluginRepo: unit.publicRepo,
1278
+ sourceType: 'marketplace-entry',
1279
+ // Production-only fields: marketplaceCommitSha and ref are frozen
1280
+ // by resolveExternalMarketplaceFreezes in the production path.
1281
+ marketplaceCommitSha: null,
1282
+ ref: null,
1283
+ payloadDigest: null,
1284
+ })
1285
+ : marketplaceForm === 'bundled-family'
1286
+ ? Object.freeze({
1287
+ form: 'bundled-family',
1288
+ repo: unit.publicRepo,
1289
+ marketplaceEntry: dist.plugin,
1290
+ pluginSubpath: '.',
1291
+ payloadDigest: null,
1292
+ })
1293
+ : null;
1261
1294
  actions.push({
1262
1295
  id: `${platform.actionType}-${unit.id}`,
1263
1296
  type: platform.actionType,
@@ -1279,6 +1312,8 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1279
1312
  // contract (whole-tree '.' containment; see plugin-marketplace).
1280
1313
  payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1281
1314
  ...(externalMarketplace ? { marketplaceLocation: 'external' } : {}),
1315
+ ...(marketplaceForm ? { marketplaceForm } : {}),
1316
+ ...(sourceDescriptor ? { sourceDescriptor } : {}),
1282
1317
  },
1283
1318
  expected: {
1284
1319
  installed: true,
@@ -1450,6 +1485,34 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1450
1485
  // snapshot, unchanged. Inline form (no marketplaceRepo) is byte-identical.
1451
1486
  const externalMarketplace = dist.marketplaceRepo !== undefined && dist.marketplaceRepo !== null;
1452
1487
  const freeze = externalMarketplace ? externalFreezes.get(`${unit.id} ${dist.type}`) : null;
1488
+ // Normalized marketplace form: explicit mutually exclusive declaration.
1489
+ const marketplaceForm = requiresMarketplace
1490
+ ? (externalMarketplace ? 'standalone-index' : 'bundled-family')
1491
+ : null;
1492
+ const sourceDescriptor = marketplaceForm === 'standalone-index'
1493
+ ? Object.freeze({
1494
+ form: 'standalone-index',
1495
+ marketplaceRepo: dist.marketplaceRepo,
1496
+ marketplaceCommitSha: freeze?.marketplaceCommitSha ?? null,
1497
+ marketplaceEntry: dist.plugin,
1498
+ // pluginRepo is the plugin's own public repo, NOT the external
1499
+ // marketplace repo. The marketplace repo contains the index; the
1500
+ // plugin repo contains the actual plugin code.
1501
+ pluginRepo: unit.publicRepo,
1502
+ sourceType: 'marketplace-entry',
1503
+ ref: freeze?.ref ?? null,
1504
+ payloadDigest: asset.manifestDigest,
1505
+ })
1506
+ : marketplaceForm === 'bundled-family'
1507
+ ? Object.freeze({
1508
+ form: 'bundled-family',
1509
+ repo: unit.publicRepo,
1510
+ commit: asset.commit,
1511
+ marketplaceEntry: dist.plugin,
1512
+ pluginSubpath: '.',
1513
+ payloadDigest: asset.manifestDigest,
1514
+ })
1515
+ : null;
1453
1516
  actions.push({
1454
1517
  id: `${platform.actionType}-${unit.id}`,
1455
1518
  type: platform.actionType,
@@ -1474,6 +1537,12 @@ export function buildExternalActions(unitResults, resolvedVersions, productionAs
1474
1537
  // contract (whole-tree '.' containment; see plugin-marketplace).
1475
1538
  payloadContract: externalMarketplace ? 'external-marketplace-v1' : 'declared-manifest-v1',
1476
1539
  ...(externalMarketplace ? { marketplaceLocation: 'external', marketplaceCommitSha: freeze.marketplaceCommitSha } : {}),
1540
+ ...(marketplaceForm ? { marketplaceForm } : {}),
1541
+ ...(sourceDescriptor ? { sourceDescriptor } : {}),
1542
+ // 冻结的插件来源提交,用于 sourceDescriptor.commit 交叉校验。
1543
+ // bundled-family: sourceDescriptor.commit 绑定到此值。
1544
+ // standalone-index: 通过此值绑定插件载荷来源。
1545
+ sourceCommit: asset.commit,
1477
1546
  },
1478
1547
  expected: {
1479
1548
  installed: true,
@@ -849,7 +849,9 @@ export async function verifyRelease(options) {
849
849
  ? 'claude-plugin'
850
850
  : action.type === 'codex-marketplace-install'
851
851
  ? 'codex-plugin'
852
- : 'kimi-plugin';
852
+ : action.type === 'codebuddy-marketplace-install'
853
+ ? 'codebuddy-plugin'
854
+ : 'kimi-plugin';
853
855
  const installPath = verifyResult.observation?.installPath;
854
856
  consumerGateResults.push(...await runConsumerVerificationGates({
855
857
  plan,
@@ -868,10 +870,14 @@ export async function verifyRelease(options) {
868
870
  HOME: resolve(runDir, 'consumers', `codex-${action.parameters.plugin}`),
869
871
  CODEX_HOME: resolve(runDir, 'consumers', `codex-${action.parameters.plugin}`),
870
872
  }
871
- : {
872
- HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
873
- KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
874
- },
873
+ : action.type === 'codebuddy-marketplace-install'
874
+ ? {
875
+ HOME: resolve(runDir, 'consumers', `codebuddy-${action.parameters.plugin}`),
876
+ }
877
+ : {
878
+ HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
879
+ KIMI_CODE_HOME: resolve(runDir, 'consumers', `kimi-${action.parameters.plugin}`),
880
+ },
875
881
  }));
876
882
  } else {
877
883
  // --- Non-marketplace: read-only adapter.verify() ---
@@ -38,11 +38,24 @@ const CONTROL_PLANE_PREFIXES = [
38
38
  '.release-skill/runs',
39
39
  '.release-skill/transactions',
40
40
  '.release-skill/kimi-attestations',
41
+ // codebuddy-attestations: same rationale as kimi-attestations above.
42
+ // Holds CodeBuddy's closure-protocol lifecycle artifacts: manual install
43
+ // requirements and human attestations bound to planDigest, payloadDigest,
44
+ // version, install path, responsible person, and expiry. Neither is
45
+ // publishable source or project configuration — they never enter the frozen
46
+ // snapshot — so excluding them keeps reconcile's own requirement output and
47
+ // the flow-required attestation from invalidating the baseline.
48
+ '.release-skill/codebuddy-attestations',
41
49
  // T3.2 incremental hook cache: a pure local optimisation written by prepare.
42
50
  // Excluding it keeps cache records from destabilising workspaceDigest on
43
51
  // every prepare (and hook-cache.mjs also skips this prefix when fingerprinting
44
52
  // inputs, so records never hash themselves).
45
53
  '.release-skill/cache',
54
+ // waivers: exception records with reason, responsible person, and expiry.
55
+ // Like attestations, they are runtime closure artifacts — not publishable
56
+ // source or project configuration — so excluding them keeps waiver writes
57
+ // from invalidating the workspace baseline.
58
+ '.release-skill/waivers',
46
59
  ];
47
60
  const RESERVED_CONTROL_PREFIXES = [
48
61
  ...CONTROL_PLANE_PREFIXES,