release-skill 0.2.1 → 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.
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 +4 -2
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +40 -0
  7. package/INSTALL.md +183 -21
  8. package/INSTALL.zh-CN.md +160 -16
  9. package/README.md +112 -13
  10. package/README.zh-CN.md +72 -11
  11. package/adapters/claude/.claude-plugin/marketplace.json +1 -1
  12. package/adapters/claude/.claude-plugin/plugin.json +1 -1
  13. package/adapters/claude/bin/release-skill.bundle.mjs +2240 -587
  14. package/adapters/claude/schemas/release-plan.schema.json +44 -2
  15. package/adapters/claude/schemas/release-project.schema.json +46 -4
  16. package/adapters/claude/schemas/release-run.schema.json +1 -0
  17. package/adapters/codex/.codex-plugin/plugin.json +2 -2
  18. package/adapters/codex/bin/release-skill.bundle.mjs +2240 -587
  19. package/adapters/codex/schemas/release-plan.schema.json +44 -2
  20. package/adapters/codex/schemas/release-project.schema.json +46 -4
  21. package/adapters/codex/schemas/release-run.schema.json +1 -0
  22. package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
  23. package/adapters/kimi/bin/release-skill.bundle.mjs +2240 -587
  24. package/adapters/kimi/schemas/release-plan.schema.json +44 -2
  25. package/adapters/kimi/schemas/release-project.schema.json +46 -4
  26. package/adapters/kimi/schemas/release-run.schema.json +1 -0
  27. package/adapters/workbuddy/.codebuddy-plugin/plugin.json +4 -2
  28. package/adapters/workbuddy/bin/release-skill.bundle.mjs +2240 -587
  29. package/adapters/workbuddy/schemas/release-plan.schema.json +44 -2
  30. package/adapters/workbuddy/schemas/release-project.schema.json +46 -4
  31. package/adapters/workbuddy/schemas/release-run.schema.json +1 -0
  32. package/bin/release-skill-cli.mjs +46 -4
  33. package/bin/release-skill.bundle.mjs +2240 -587
  34. package/package.json +1 -1
  35. package/references/02-project-config.md +7 -0
  36. package/references/06-adapter-contract.md +21 -2
  37. package/schemas/release-plan.schema.json +44 -2
  38. package/schemas/release-project.schema.json +46 -4
  39. package/schemas/release-run.schema.json +1 -0
  40. package/scripts/sync-public-files.mjs +8 -4
  41. package/src/adapters/contract.mjs +1 -0
  42. package/src/adapters/plugin-marketplace.mjs +796 -41
  43. package/src/commands/assess.mjs +50 -1
  44. package/src/commands/prepare.mjs +342 -9
  45. package/src/commands/publish.mjs +1 -0
  46. package/src/commands/reconcile.mjs +1 -0
  47. package/src/commands/setup.mjs +7 -3
  48. package/src/commands/verify.mjs +13 -5
  49. package/src/core/baseline.mjs +13 -0
  50. package/src/core/checkpoints.mjs +7 -2
  51. package/src/core/plan.mjs +275 -4
  52. package/src/core/verification-gates.mjs +1 -1
  53. package/src/platforms/codebuddy.mjs +658 -0
  54. package/src/platforms/registry.mjs +258 -5
  55. package/src/producers/build-adapters.mjs +30 -15
@@ -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,
@@ -41,6 +41,17 @@ import {
41
41
  validateKimiAttestation,
42
42
  readKimiManifest,
43
43
  } from '../platforms/kimi.mjs';
44
+ import {
45
+ CODEBUDDY_REQUIREMENT_FILE,
46
+ CODEBUDDY_ATTESTATION_FILE,
47
+ CODEBUDDY_MARKETPLACE_NAME,
48
+ CODEBUDDY_MARKETPLACE_SOURCE,
49
+ resolveCodeBuddyBoundPlanDigest,
50
+ codebuddyAuthorityDir,
51
+ codebuddyCliHome,
52
+ codebuddyCliInstallRoot,
53
+ validateCodeBuddyAttestation,
54
+ } from '../platforms/codebuddy.mjs';
44
55
 
45
56
  const execFile = promisify(execFileCb);
46
57
 
@@ -67,6 +78,15 @@ function transportPayload(entries) {
67
78
  * Actions without the marker keep the legacy full-tree equality semantics.
68
79
  */
69
80
  const PAYLOAD_CONTRACT_DECLARED_MANIFEST = 'declared-manifest-v1';
81
+ /**
82
+ * External independent marketplace contract. The marketplace index lives in an
83
+ * external repository (frozen by prepare via marketplaceCommitSha + add-ref);
84
+ * the unit snapshot carries only the plugin manifest. The installed payload is
85
+ * verified by whole-tree ('.') containment with the same semantics as
86
+ * declared-manifest-v1 (every authority file present and byte-identical;
87
+ * host-added files recorded as extraInstalledPaths, not failed).
88
+ */
89
+ const PAYLOAD_CONTRACT_EXTERNAL_MARKETPLACE = 'external-marketplace-v1';
70
90
  /** Audit cap: at most this many extra installed paths are recorded. */
71
91
  const EXTRA_INSTALLED_PATHS_CAP = 200;
72
92
  /** Diagnostic cap: at most this many conflict paths are listed per error. */
@@ -132,6 +152,17 @@ function extractDeclaredPluginSource(consumer, entry) {
132
152
  * tampering with it fails the snapshot digest revalidation first.
133
153
  */
134
154
  async function resolveInstalledPayloadSubpath(snapshotDir, sourceEntries, action, consumer) {
155
+ // External independent marketplace form: the marketplace index lives in the
156
+ // external repository, not the unit snapshot (which may carry no marketplace
157
+ // manifest). The whole snapshot is the installed payload — short-circuit to
158
+ // "." without reading any marketplace manifest. kimi/codebuddy/inline never
159
+ // carry these markers, so they fall through to their existing branches below.
160
+ if (
161
+ action.payloadContract === PAYLOAD_CONTRACT_EXTERNAL_MARKETPLACE
162
+ || action.marketplaceLocation === 'external'
163
+ ) {
164
+ return '.';
165
+ }
135
166
  // Platforms without a marketplace manifest (kimi) install the whole
136
167
  // snapshot as the payload.
137
168
  const marketplaceRelative = getPlatform(consumer).manifestPaths.marketplace;
@@ -199,15 +230,26 @@ async function verifyInstalledMarketplacePayload(action, context, installPath, c
199
230
  // plans without the marker keep the legacy full-tree equality semantics
200
231
  // byte-for-byte (including the consumer transport exclusion list).
201
232
  const payloadContract = action.payloadContract;
202
- if (payloadContract !== undefined && payloadContract !== PAYLOAD_CONTRACT_DECLARED_MANIFEST) {
233
+ if (
234
+ payloadContract !== undefined
235
+ && payloadContract !== PAYLOAD_CONTRACT_DECLARED_MANIFEST
236
+ && payloadContract !== PAYLOAD_CONTRACT_EXTERNAL_MARKETPLACE
237
+ ) {
203
238
  throw new Error(`unsupported marketplace payload contract: ${JSON.stringify(payloadContract)}`);
204
239
  }
205
- if (payloadContract === PAYLOAD_CONTRACT_DECLARED_MANIFEST) {
206
- // declared-manifest-v1: every authority entry must exist in the installed
207
- // payload and agree in type/size/bytes/non-write mode bits. Host-added
208
- // files are NOT failures — they are recorded (relative paths, capped) as
209
- // audit evidence so host evolution can never break a release, while any
210
- // missing or altered declared file still fails closed.
240
+ if (
241
+ payloadContract === PAYLOAD_CONTRACT_DECLARED_MANIFEST
242
+ || payloadContract === PAYLOAD_CONTRACT_EXTERNAL_MARKETPLACE
243
+ ) {
244
+ // declared-manifest-v1 / external-marketplace-v1: every authority entry
245
+ // must exist in the installed payload and agree in
246
+ // type/size/bytes/non-write mode bits. Host-added files are NOT failures —
247
+ // they are recorded (relative paths, capped) as audit evidence so host
248
+ // evolution can never break a release, while any missing or altered
249
+ // declared file still fails closed. The two contracts share this
250
+ // containment semantics; they differ only in the authority subtree source
251
+ // (declared-manifest reads the entry's declared source subpath; external
252
+ // short-circuits to the whole tree '.').
211
253
  const installedSnapshot = await computeFrozenSnapshot(installPath);
212
254
  const authorityPayload = transportPayload(authorityEntries);
213
255
  const installedByPath = new Map(
@@ -237,7 +279,7 @@ async function verifyInstalledMarketplacePayload(action, context, installPath, c
237
279
  ? `; and ${conflicts.length - PAYLOAD_CONFLICT_REPORT_CAP} more conflicting path(s)`
238
280
  : '';
239
281
  throw new Error(
240
- `installed marketplace payload differs in path, bytes, size, or non-write mode bits (${PAYLOAD_CONTRACT_DECLARED_MANIFEST}): ${listed}${overflow}`,
282
+ `installed marketplace payload differs in path, bytes, size, or non-write mode bits (${payloadContract}): ${listed}${overflow}`,
241
283
  );
242
284
  }
243
285
  const authorityPaths = new Set(authorityPayload.map((entry) => entry.path));
@@ -395,17 +437,139 @@ async function resolveKimiEntrySkillFile(pluginRootReal, manifest, entrySkill) {
395
437
  return entryReal;
396
438
  }
397
439
 
440
+ /**
441
+ * CodeBuddy equivalent of normalizeKimiSkillsRel (parallel implementation,
442
+ * codebuddy wording; the kimi helper above is intentionally left untouched so
443
+ * its error strings stay byte-identical). Validates a manifest `skills` value
444
+ * as a safe plugin-root-relative path and returns the normalized root-relative
445
+ * path (no leading "./", no trailing "/").
446
+ *
447
+ * @param {string} skillsRaw
448
+ * @returns {string} normalized relative path ('' for the plugin root itself)
449
+ */
450
+ function normalizeCodeBuddySkillsRel(skillsRaw) {
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) {
468
+ throw new Error('codebuddy manifest skills must be a non-empty relative path when present');
469
+ }
470
+ if (
471
+ skillsPath.startsWith('/') ||
472
+ skillsPath.includes('..') ||
473
+ skillsPath.includes('\\') ||
474
+ /^https?:\/\//i.test(skillsPath)
475
+ ) {
476
+ throw new Error(`codebuddy manifest skills "${skillsPath}" is not a safe relative path`);
477
+ }
478
+ let rel = skillsPath.replace(/^\.\//, '');
479
+ rel = rel.replace(/\/+$/, '');
480
+ if (rel.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
481
+ throw new Error(`codebuddy manifest skills "${skillsPath}" is not a safe relative path`);
482
+ }
483
+ return rel;
484
+ }
485
+
486
+ /**
487
+ * CodeBuddy equivalent of resolveKimiEntrySkillFile (parallel implementation,
488
+ * codebuddy wording; the kimi helper above is intentionally left untouched).
489
+ * Resolves the entry SKILL.md from the authoritative codebuddy manifest:
490
+ * - declared `skills`: entry resolves under that validated, realpath-contained
491
+ * skills root;
492
+ * - omitted `skills`: the plugin root's own SKILL.md is the single skill.
493
+ * The returned path is realpath-contained within pluginRootReal and is a
494
+ * regular, non-symlink file. Throws on missing/escaping/invalid layouts so the
495
+ * caller fails closed.
496
+ *
497
+ * @param {string} pluginRootReal - realpath of the verified plugin root.
498
+ * @param {object} manifest - parsed codebuddy plugin manifest.
499
+ * @param {string} entrySkill - expected entry skill id.
500
+ * @returns {Promise<string>} realpath of the entry SKILL.md
501
+ */
502
+ async function resolveCodeBuddyEntrySkillFile(pluginRootReal, manifest, entrySkill) {
503
+ if (!entrySkill || typeof entrySkill !== 'string' || !SAFE_ID_RE.test(entrySkill)) {
504
+ throw new Error(`unsafe entrySkill: "${entrySkill}"`);
505
+ }
506
+ let entryAbs;
507
+ if (manifest.skills === undefined || manifest.skills === null) {
508
+ // Official single-skill semantics: root SKILL.md is the sole skill.
509
+ entryAbs = resolve(pluginRootReal, 'SKILL.md');
510
+ } else {
511
+ const skillsRel = normalizeCodeBuddySkillsRel(manifest.skills);
512
+ const skillsRootAbs = skillsRel === '' ? pluginRootReal : resolve(pluginRootReal, skillsRel);
513
+ const skillsRootReal = await realpath(skillsRootAbs).catch(() => null);
514
+ if (!skillsRootReal) {
515
+ throw new Error(`codebuddy manifest skills root does not exist: ${manifest.skills}`);
516
+ }
517
+ const skillsContainment = relative(pluginRootReal, skillsRootReal);
518
+ const sepK = process.platform === 'win32' ? '\\' : '/';
519
+ if (
520
+ skillsContainment !== '' &&
521
+ (isAbsolute(skillsContainment) || skillsContainment === '..' || skillsContainment.startsWith(`..${sepK}`))
522
+ ) {
523
+ throw new Error(`codebuddy manifest skills "${manifest.skills}" escapes the plugin root after symlink resolution`);
524
+ }
525
+ entryAbs = resolve(skillsRootReal, entrySkill, 'SKILL.md');
526
+ }
527
+
528
+ // lstat the LEXICAL entry BEFORE realpath: a symlinked SKILL.md must be
529
+ // rejected outright (lstat-ing the resolved target would miss the symlink).
530
+ let entryLexicalStat;
531
+ try {
532
+ entryLexicalStat = await lstat(entryAbs);
533
+ } catch {
534
+ throw new Error(`codebuddy entry skill not found: ${relative(pluginRootReal, entryAbs) || 'SKILL.md'}`);
535
+ }
536
+ if (entryLexicalStat.isSymbolicLink()) {
537
+ throw new Error('codebuddy entry skill must not be a symlink');
538
+ }
539
+ if (!entryLexicalStat.isFile()) {
540
+ throw new Error('codebuddy entry skill is not a regular file');
541
+ }
542
+
543
+ const entryReal = await realpath(entryAbs).catch(() => null);
544
+ if (!entryReal) {
545
+ throw new Error(`codebuddy entry skill not found: ${relative(pluginRootReal, entryAbs) || 'SKILL.md'}`);
546
+ }
547
+ const entryContainment = relative(pluginRootReal, entryReal);
548
+ const sepE = process.platform === 'win32' ? '\\' : '/';
549
+ if (
550
+ entryContainment !== '' &&
551
+ (isAbsolute(entryContainment) || entryContainment === '..' || entryContainment.startsWith(`..${sepE}`))
552
+ ) {
553
+ throw new Error('codebuddy entry skill escapes the plugin root after symlink resolution');
554
+ }
555
+ return entryReal;
556
+ }
557
+
398
558
  const SUPPORTED_TYPES = [
399
559
  ActionType.PLUGIN_MANIFEST_VALIDATE,
400
560
  ActionType.PLUGIN_INSTALL_CHECK,
401
561
  ActionType.CLAUDE_MARKETPLACE_INSTALL,
402
562
  ActionType.CODEX_MARKETPLACE_INSTALL,
403
563
  ActionType.KIMI_MARKETPLACE_INSTALL,
564
+ ActionType.CODEBUDDY_MARKETPLACE_INSTALL,
404
565
  ];
405
566
 
406
567
  /** Safe repo pattern: owner/repo with alphanumeric, hyphens, dots, underscores. */
407
568
  const SAFE_REPO_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
408
569
 
570
+ /** Safe digest pattern: 64-hex SHA-256. */
571
+ const SAFE_DIGEST_RE = /^[0-9a-f]{64}$/;
572
+
409
573
  /** Valid consumer ids, derived from the platform registry (single source). */
410
574
  const CONSUMER_IDS = new Set(PLATFORMS.map((p) => p.id));
411
575
 
@@ -489,7 +653,9 @@ function validateMarketplaceParams(params) {
489
653
  // marketplace but NO non-interactive install API, so `marketplace` carries no
490
654
  // executable meaning for kimi and is optional (validated only if present); it
491
655
  // must not become a required identity condition for kimi execution/observe.
492
- if (!getPlatform(consumer).automatable) {
656
+ const consumerPlatform = getPlatform(consumer);
657
+ const consumerRoute = resolvePlatformRoute(consumerPlatform);
658
+ if (consumerRoute.route === 'human-attestation') {
493
659
  if (marketplace !== undefined && marketplace !== null && !SAFE_ID_RE.test(marketplace)) {
494
660
  return { valid: false, error: `unsafe marketplace identifier: "${marketplace}"` };
495
661
  }
@@ -662,7 +828,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
662
828
  if (
663
829
  actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
664
830
  actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
665
- actionType === ActionType.KIMI_MARKETPLACE_INSTALL
831
+ actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
832
+ actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
666
833
  ) {
667
834
  // 1. Validate all parameters for injection safety
668
835
  const validation = validateMarketplaceParams(action);
@@ -735,9 +902,224 @@ export function createPluginMarketplaceAdapter(deps = {}) {
735
902
  });
736
903
  }
737
904
 
738
- // 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.
739
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
740
1121
  const platform = getPlatform(consumer);
1122
+ const platformRoute = resolvePlatformRoute(platform);
741
1123
  let snapshotDirReal;
742
1124
  // Authoritative kimi manifest (from the frozen snapshot), used to
743
1125
  // resolve the entry skill via the manifest-declared skills root.
@@ -752,12 +1134,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
752
1134
  });
753
1135
  }
754
1136
 
755
- // A non-automatable platform (kimi) has no non-interactive
756
- // marketplace/install API: the whole repo is installed as one
757
- // plugin. The authoritative manifest is read from the verified
758
- // snapshot root via the platform strategy's official precedence
759
- // (kimi.plugin.json over .kimi-plugin/plugin.json).
760
- if (!platform.automatable) {
1137
+ // A human-attestation platform (kimi, codebuddy) has no trustworthy
1138
+ // automated install: the whole repo is installed as one plugin. The
1139
+ // authoritative manifest is read from the verified snapshot root via
1140
+ // the platform strategy (kimi: kimi.plugin.json over
1141
+ // .kimi-plugin/plugin.json; codebuddy: .codebuddy-plugin/plugin.json).
1142
+ if (platformRoute.route === 'human-attestation') {
1143
+ // Error-message label keeps the kimi wording byte-identical while
1144
+ // giving codebuddy its own wording.
1145
+ const manifestLabel = consumer === 'codebuddy' ? 'codebuddy' : 'kimi';
761
1146
  let kimiManifestResult;
762
1147
  try {
763
1148
  kimiManifestResult = await platform.strategy.readManifest(snapshotDirReal);
@@ -765,7 +1150,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
765
1150
  return createResult({
766
1151
  actionType,
767
1152
  status: ActionStatus.PREFLIGHT_FAILED,
768
- error: `frozen snapshot kimi manifest invalid: ${manifestErr.message}`,
1153
+ error: `frozen snapshot ${manifestLabel} manifest invalid: ${manifestErr.message}`,
769
1154
  });
770
1155
  }
771
1156
  const kimiManifest = kimiManifestResult.manifest;
@@ -786,7 +1171,60 @@ export function createPluginMarketplaceAdapter(deps = {}) {
786
1171
  kimiSnapshotManifest = kimiManifest;
787
1172
  }
788
1173
 
789
- if (platform.automatable) {
1174
+ if (platformRoute.route === 'structured-cli') {
1175
+ if (action.marketplaceLocation === 'external') {
1176
+ // External independent marketplace form: the marketplace index lives
1177
+ // in the external repository (frozen by prepare), NOT in the unit
1178
+ // snapshot, so the snapshot marketplace-manifest section is skipped
1179
+ // entirely. The plugin manifest is read directly from the snapshot
1180
+ // root and the frozen external identity fields are validated. ref was
1181
+ // already injection-checked above (step 2); the entry skill reuses the
1182
+ // automatable fixed layout validated below.
1183
+ if (
1184
+ typeof action.marketplaceCommitSha !== 'string'
1185
+ || !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)
1186
+ ) {
1187
+ return createResult({
1188
+ actionType,
1189
+ status: ActionStatus.PREFLIGHT_FAILED,
1190
+ error: `external marketplace marketplaceCommitSha must be a 40-hex commit sha, got ${JSON.stringify(action.marketplaceCommitSha)}`,
1191
+ });
1192
+ }
1193
+ if (
1194
+ typeof action.repo !== 'string'
1195
+ || !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(action.repo)
1196
+ ) {
1197
+ return createResult({
1198
+ actionType,
1199
+ status: ActionStatus.PREFLIGHT_FAILED,
1200
+ error: `external marketplace repo must be an owner/name repository, got ${JSON.stringify(action.repo)}`,
1201
+ });
1202
+ }
1203
+ const externalManifestRelative = platform.manifestPaths.plugin;
1204
+ const externalManifestPath = resolve(snapshotDirReal, externalManifestRelative);
1205
+ const externalManifestResult = await validateManifestFile(externalManifestPath, ['name', 'version']);
1206
+ if (!externalManifestResult.valid) {
1207
+ return createResult({
1208
+ actionType,
1209
+ status: ActionStatus.PREFLIGHT_FAILED,
1210
+ error: `frozen snapshot ${externalManifestRelative} invalid: ${externalManifestResult.error}`,
1211
+ });
1212
+ }
1213
+ if (externalManifestResult.manifest.name !== action.plugin) {
1214
+ return createResult({
1215
+ actionType,
1216
+ status: ActionStatus.PREFLIGHT_FAILED,
1217
+ error: `plugin manifest name "${externalManifestResult.manifest.name}" does not match action plugin "${action.plugin}"`,
1218
+ });
1219
+ }
1220
+ if (externalManifestResult.manifest.version !== action.version) {
1221
+ return createResult({
1222
+ actionType,
1223
+ status: ActionStatus.PREFLIGHT_FAILED,
1224
+ error: `plugin manifest version "${externalManifestResult.manifest.version}" does not match action version "${action.version}"`,
1225
+ });
1226
+ }
1227
+ } else {
790
1228
  // Verify marketplace files exist.
791
1229
  // marketplace.json is at the snapshot root; plugin manifest is
792
1230
  // resolved relative to the entry's declared source path.
@@ -923,17 +1361,21 @@ export function createPluginMarketplaceAdapter(deps = {}) {
923
1361
  });
924
1362
  }
925
1363
  }
1364
+ }
926
1365
 
927
1366
  // Verify the entry skill exists in the snapshot.
928
- // Automatable platforms' manifests always declare ./skills/, so the
1367
+ // Structured-cli platforms' manifests always declare ./skills/, so the
929
1368
  // fixed skills/<entrySkill>/SKILL.md layout is authoritative for
930
- // them. A non-automatable platform (kimi) resolves the entry skill
931
- // via the manifest-declared skills root (MAJOR-4): the root is
932
- // validated + realpath-contained, and omitted `skills` means the
933
- // official single-skill root SKILL.md.
934
- if (!platform.automatable) {
1369
+ // them. A human-attestation platform (kimi, codebuddy) resolves the
1370
+ // entry skill via the manifest-declared skills root (MAJOR-4): the
1371
+ // root is validated + realpath-contained, and omitted `skills` means
1372
+ // the official single-skill root SKILL.md.
1373
+ if (platformRoute.route === 'human-attestation') {
1374
+ const resolveEntrySkillFile = consumer === 'codebuddy'
1375
+ ? resolveCodeBuddyEntrySkillFile
1376
+ : resolveKimiEntrySkillFile;
935
1377
  try {
936
- await resolveKimiEntrySkillFile(snapshotDirReal, kimiSnapshotManifest, action.entrySkill);
1378
+ await resolveEntrySkillFile(snapshotDirReal, kimiSnapshotManifest, action.entrySkill);
937
1379
  } catch (entryErr) {
938
1380
  return createResult({
939
1381
  actionType,
@@ -1100,7 +1542,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1100
1542
  if (
1101
1543
  actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
1102
1544
  actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
1103
- actionType === ActionType.KIMI_MARKETPLACE_INSTALL
1545
+ actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
1546
+ actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
1104
1547
  ) {
1105
1548
  try {
1106
1549
  assertIsolatedConsumerWritesAuthorized(context, actionType);
@@ -1134,10 +1577,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1134
1577
  // It is handled entirely by its manual-requirement strategy, which
1135
1578
  // uses a stable plan-digest-keyed home and deliberately SKIPS the
1136
1579
  // per-run isolated consumer dir and its runDir containment check
1137
- // (that model only fits automatable platforms, which exec a real
1580
+ // (that model only fits structured-cli platforms, which exec a real
1138
1581
  // CLI into a per-run HOME).
1139
1582
  const platform = getPlatform(action.consumer);
1140
- if (!platform.automatable) {
1583
+ const executeRoute = resolvePlatformRoute(platform);
1584
+ if (executeRoute.route === 'human-attestation') {
1141
1585
  return platform.strategy.buildManualRequirement(action, context);
1142
1586
  }
1143
1587
 
@@ -1442,7 +1886,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1442
1886
  if (
1443
1887
  actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
1444
1888
  actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
1445
- actionType === ActionType.KIMI_MARKETPLACE_INSTALL
1889
+ actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
1890
+ actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
1446
1891
  ) {
1447
1892
  const consumer = action.consumer;
1448
1893
  const runDir = context.runDir;
@@ -1454,19 +1899,22 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1454
1899
  });
1455
1900
  }
1456
1901
  const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
1457
- // Registry-driven platform data. observe historically applies no
1458
- // consumer validation gate (execute/preflight validate it), so an
1459
- // unregistered consumer keeps the legacy fall-through shape: a
1460
- // kimi-shaped env, no attestation branch, and no CLI binary — it
1461
- // 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.
1462
1906
  const platform = PLATFORMS.find((p) => p.id === consumer) ?? null;
1463
- 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;
1464
1914
  const baseEnv = { ...process.env, ...(context.env ?? {}) };
1465
1915
  const env = {
1466
1916
  ...baseEnv,
1467
- ...(platform
1468
- ? platform.isolationEnv(isolatedHome)
1469
- : { HOME: isolatedHome, KIMI_CODE_HOME: isolatedHome }),
1917
+ ...platform.isolationEnv(isolatedHome),
1470
1918
  };
1471
1919
 
1472
1920
  // Resolve frozen timeoutMs from the expanded action (top-level).
@@ -1493,7 +1941,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1493
1941
  // root (MAJOR-4), and manifest name/version.
1494
1942
  // Missing/expired/mismatched/escaping proof fails closed so a kimi
1495
1943
  // unit can never reach VERIFIED without it.
1496
- if (platform && !platform.automatable) {
1944
+ const kimiRoute = platform ? resolvePlatformRoute(platform) : null;
1945
+ if (platform && kimiRoute?.route === 'human-attestation' && actionType === ActionType.KIMI_MARKETPLACE_INSTALL) {
1497
1946
  const expectedRef = action.ref ?? `v${action.version}`;
1498
1947
 
1499
1948
  // Bind to the REAL frozen plan digest (A). Fail closed if the
@@ -1774,6 +2223,302 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1774
2223
  });
1775
2224
  }
1776
2225
 
2226
+ // 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.
2238
+ const codebuddyRoute = platform ? resolvePlatformRoute(platform) : null;
2239
+ if (platform && codebuddyRoute?.route === 'human-attestation' && actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL) {
2240
+ const expectedRef = action.ref ?? `v${action.version}`;
2241
+
2242
+ // Bind to the REAL frozen plan digest (A). Fail closed if the
2243
+ // context does not carry an intact frozen plan.
2244
+ let boundPlanDigest;
2245
+ try {
2246
+ boundPlanDigest = await resolveCodeBuddyBoundPlanDigest(context);
2247
+ } catch (planErr) {
2248
+ return createResult({
2249
+ actionType,
2250
+ status: ActionStatus.OBSERVED,
2251
+ observation: {
2252
+ installed: false,
2253
+ error: `cannot bind codebuddy observation to the frozen plan: ${planErr.message}`,
2254
+ },
2255
+ });
2256
+ }
2257
+
2258
+ // 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).
2261
+ let attestationDir;
2262
+ try {
2263
+ attestationDir = codebuddyAuthorityDir(context, boundPlanDigest, action.plugin);
2264
+ } catch (dirErr) {
2265
+ return createResult({
2266
+ actionType,
2267
+ status: ActionStatus.OBSERVED,
2268
+ observation: { installed: false, error: dirErr.message },
2269
+ });
2270
+ }
2271
+
2272
+ // The execute-emitted requirement must exist and bind to the action
2273
+ // and the frozen plan digest.
2274
+ let requirement = null;
2275
+ try {
2276
+ requirement = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_REQUIREMENT_FILE), 'utf8'));
2277
+ } catch {
2278
+ return createResult({
2279
+ actionType,
2280
+ status: ActionStatus.OBSERVED,
2281
+ observation: {
2282
+ installed: false,
2283
+ manualInstallRequired: true,
2284
+ error: 'codebuddy manual-install requirement is missing; run execute first',
2285
+ },
2286
+ });
2287
+ }
2288
+ if (
2289
+ requirement.planDigest !== boundPlanDigest ||
2290
+ requirement.plugin !== action.plugin ||
2291
+ requirement.version !== action.version ||
2292
+ requirement.entrySkill !== action.entrySkill ||
2293
+ requirement.repo !== action.repo ||
2294
+ requirement.ref !== expectedRef
2295
+ ) {
2296
+ return createResult({
2297
+ actionType,
2298
+ status: ActionStatus.OBSERVED,
2299
+ observation: {
2300
+ installed: false,
2301
+ error: 'codebuddy manual-install requirement does not match the frozen plan/action',
2302
+ },
2303
+ });
2304
+ }
2305
+
2306
+ // The trusted human attestation is mandatory and is read from the
2307
+ // stable authority dir. Without it the manual install has not been
2308
+ // proven: fail closed.
2309
+ let attestation = null;
2310
+ try {
2311
+ attestation = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE), 'utf8'));
2312
+ } catch {
2313
+ return createResult({
2314
+ actionType,
2315
+ status: ActionStatus.OBSERVED,
2316
+ observation: {
2317
+ installed: false,
2318
+ manualInstallRequired: true,
2319
+ marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
2320
+ attestationDir,
2321
+ error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install (marketplace ${CODEBUDDY_MARKETPLACE_SOURCE})`,
2322
+ },
2323
+ });
2324
+ }
2325
+
2326
+ const attestationCheck = validateCodeBuddyAttestation(attestation, action, new Date().toISOString(), boundPlanDigest);
2327
+ if (!attestationCheck.valid) {
2328
+ return createResult({
2329
+ actionType,
2330
+ status: ActionStatus.OBSERVED,
2331
+ observation: { installed: false, error: attestationCheck.error },
2332
+ });
2333
+ }
2334
+
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) {
2369
+ return createResult({
2370
+ actionType,
2371
+ status: ActionStatus.OBSERVED,
2372
+ observation: { installed: false, error: `codebuddy install path cannot be resolved: ${attestation.installPath}` },
2373
+ });
2374
+ }
2375
+
2376
+ if (attestation.installChannel === 'cli') {
2377
+ const cliHome = codebuddyCliHome(attestationDir);
2378
+ const cliHomeReal = await realpath(cliHome).catch(() => null);
2379
+ if (!cliHomeReal) {
2380
+ return createResult({
2381
+ actionType,
2382
+ status: ActionStatus.OBSERVED,
2383
+ observation: {
2384
+ installed: false,
2385
+ error: `codebuddy isolated CLI home does not exist or cannot be resolved: ${cliHome}`,
2386
+ },
2387
+ });
2388
+ }
2389
+ const cliRootAbs = codebuddyCliInstallRoot(cliHomeReal, attestation.marketplace, action.plugin);
2390
+ const cliRootReal = await realpath(cliRootAbs).catch(() => null);
2391
+ if (!cliRootReal) {
2392
+ return createResult({
2393
+ actionType,
2394
+ status: ActionStatus.OBSERVED,
2395
+ observation: {
2396
+ installed: false,
2397
+ error: `codebuddy CLI marketplace plugin root does not exist: ${cliRootAbs}`,
2398
+ },
2399
+ });
2400
+ }
2401
+ const sepC = process.platform === 'win32' ? '\\' : '/';
2402
+ const relToRoot = relative(cliRootReal, installPathReal);
2403
+ if (
2404
+ relToRoot !== '' &&
2405
+ (isAbsolute(relToRoot) || relToRoot === '..' || relToRoot.startsWith(`..${sepC}`))
2406
+ ) {
2407
+ return createResult({
2408
+ actionType,
2409
+ status: ActionStatus.OBSERVED,
2410
+ observation: {
2411
+ installed: false,
2412
+ error: `codebuddy install path escapes the isolated CLI marketplace root (${cliRootReal}): ${attestation.installPath}`,
2413
+ },
2414
+ });
2415
+ }
2416
+ }
2417
+
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
+ }
2447
+
2448
+ // Entry skill must resolve via the installed manifest's skills root,
2449
+ // with the codebuddy single-candidate manifest.
2450
+ let installedManifest;
2451
+ 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
+ });
2467
+ }
2468
+
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
+ });
2495
+ }
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
+ }
2521
+
1777
2522
  // Read execute evidence — mandatory for observe validation
1778
2523
  let evidence = null;
1779
2524
  try {
@@ -2075,6 +2820,16 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2075
2820
  if (evidence.repo) observation.repo = evidence.repo;
2076
2821
  if (evidence.ref) observation.ref = evidence.ref;
2077
2822
 
2823
+ // External independent marketplace form: bind the frozen external
2824
+ // identity markers so verify's expected subset matches. These are
2825
+ // frozen plan identity (like repo/ref), never re-derived from the
2826
+ // remote at observe time; the install-side entry observation above
2827
+ // (CLI list version binding) compensates for the weak name-freeze.
2828
+ if (action.marketplaceLocation === 'external') {
2829
+ observation.marketplaceLocation = action.marketplaceLocation;
2830
+ observation.marketplaceCommitSha = action.marketplaceCommitSha;
2831
+ }
2832
+
2078
2833
  return createResult({
2079
2834
  actionType,
2080
2835
  status: ActionStatus.OBSERVED,