release-skill 0.2.0 → 0.2.2

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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codebuddy-plugin/plugin.json +10 -0
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.kimi-plugin/plugin.json +1 -1
  6. package/CHANGELOG.md +35 -0
  7. package/INSTALL.md +186 -4
  8. package/INSTALL.zh-CN.md +166 -4
  9. package/README.md +116 -13
  10. package/README.zh-CN.md +73 -13
  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 +1718 -600
  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 +1718 -600
  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 +1718 -600
  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 +12 -0
  28. package/adapters/workbuddy/bin/release-skill.bundle.mjs +86481 -0
  29. package/adapters/workbuddy/bin/release-skill.mjs +54 -0
  30. package/adapters/workbuddy/native/safe-write/binding.gyp +41 -0
  31. package/adapters/workbuddy/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
  32. package/adapters/workbuddy/native/safe-write/prebuilds.json +24 -0
  33. package/adapters/workbuddy/native/safe-write/src/safe_write.cc +2032 -0
  34. package/adapters/workbuddy/schemas/.render-manifest.json +37 -0
  35. package/adapters/workbuddy/schemas/approval-record.schema.json +115 -0
  36. package/adapters/workbuddy/schemas/artifact-lock.schema.json +111 -0
  37. package/adapters/workbuddy/schemas/artifact-plan.schema.json +52 -0
  38. package/adapters/workbuddy/schemas/artifact-policy.schema.json +76 -0
  39. package/adapters/workbuddy/schemas/evidence-event.schema.json +89 -0
  40. package/adapters/workbuddy/schemas/release-plan.schema.json +924 -0
  41. package/adapters/workbuddy/schemas/release-project.schema.json +951 -0
  42. package/adapters/workbuddy/schemas/release-run.schema.json +344 -0
  43. package/adapters/workbuddy/skills/release-assess/SKILL.md +51 -0
  44. package/adapters/workbuddy/skills/release-help/SKILL.md +77 -0
  45. package/adapters/workbuddy/skills/release-prepare/SKILL.md +92 -0
  46. package/adapters/workbuddy/skills/release-publish/SKILL.md +57 -0
  47. package/adapters/workbuddy/skills/release-reconcile/SKILL.md +73 -0
  48. package/adapters/workbuddy/skills/release-setup/SKILL.md +95 -0
  49. package/adapters/workbuddy/skills/release-verify/SKILL.md +70 -0
  50. package/bin/release-skill-cli.mjs +46 -4
  51. package/bin/release-skill.bundle.mjs +1718 -600
  52. package/package.json +2 -1
  53. package/references/02-project-config.md +7 -0
  54. package/references/06-adapter-contract.md +21 -2
  55. package/schemas/release-plan.schema.json +44 -2
  56. package/schemas/release-project.schema.json +46 -4
  57. package/schemas/release-run.schema.json +1 -0
  58. package/scripts/sync-public-files.mjs +27 -4
  59. package/src/adapters/contract.mjs +1 -0
  60. package/src/adapters/plugin-marketplace.mjs +536 -23
  61. package/src/commands/assess.mjs +50 -1
  62. package/src/commands/prepare.mjs +273 -9
  63. package/src/commands/publish.mjs +1 -0
  64. package/src/commands/reconcile.mjs +1 -0
  65. package/src/commands/setup.mjs +7 -3
  66. package/src/commands/verify.mjs +2 -0
  67. package/src/core/checkpoints.mjs +7 -2
  68. package/src/core/plan.mjs +97 -4
  69. package/src/core/verification-gates.mjs +1 -1
  70. package/src/platforms/codebuddy.mjs +618 -0
  71. package/src/platforms/registry.mjs +100 -5
  72. package/src/producers/build-adapters.mjs +48 -6
@@ -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,12 +437,115 @@ 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
+ if (typeof skillsRaw !== 'string' || skillsRaw.length === 0) {
452
+ throw new Error('codebuddy manifest skills must be a non-empty relative path when present');
453
+ }
454
+ if (
455
+ skillsRaw.startsWith('/') ||
456
+ skillsRaw.includes('..') ||
457
+ skillsRaw.includes('\\') ||
458
+ /^https?:\/\//i.test(skillsRaw)
459
+ ) {
460
+ throw new Error(`codebuddy manifest skills "${skillsRaw}" is not a safe relative path`);
461
+ }
462
+ let rel = skillsRaw.replace(/^\.\//, '');
463
+ rel = rel.replace(/\/+$/, '');
464
+ if (rel.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
465
+ throw new Error(`codebuddy manifest skills "${skillsRaw}" is not a safe relative path`);
466
+ }
467
+ return rel;
468
+ }
469
+
470
+ /**
471
+ * CodeBuddy equivalent of resolveKimiEntrySkillFile (parallel implementation,
472
+ * codebuddy wording; the kimi helper above is intentionally left untouched).
473
+ * Resolves the entry SKILL.md from the authoritative codebuddy manifest:
474
+ * - declared `skills`: entry resolves under that validated, realpath-contained
475
+ * skills root;
476
+ * - omitted `skills`: the plugin root's own SKILL.md is the single skill.
477
+ * The returned path is realpath-contained within pluginRootReal and is a
478
+ * regular, non-symlink file. Throws on missing/escaping/invalid layouts so the
479
+ * caller fails closed.
480
+ *
481
+ * @param {string} pluginRootReal - realpath of the verified plugin root.
482
+ * @param {object} manifest - parsed codebuddy plugin manifest.
483
+ * @param {string} entrySkill - expected entry skill id.
484
+ * @returns {Promise<string>} realpath of the entry SKILL.md
485
+ */
486
+ async function resolveCodeBuddyEntrySkillFile(pluginRootReal, manifest, entrySkill) {
487
+ if (!entrySkill || typeof entrySkill !== 'string' || !SAFE_ID_RE.test(entrySkill)) {
488
+ throw new Error(`unsafe entrySkill: "${entrySkill}"`);
489
+ }
490
+ let entryAbs;
491
+ if (manifest.skills === undefined || manifest.skills === null) {
492
+ // Official single-skill semantics: root SKILL.md is the sole skill.
493
+ entryAbs = resolve(pluginRootReal, 'SKILL.md');
494
+ } else {
495
+ const skillsRel = normalizeCodeBuddySkillsRel(manifest.skills);
496
+ const skillsRootAbs = skillsRel === '' ? pluginRootReal : resolve(pluginRootReal, skillsRel);
497
+ const skillsRootReal = await realpath(skillsRootAbs).catch(() => null);
498
+ if (!skillsRootReal) {
499
+ throw new Error(`codebuddy manifest skills root does not exist: ${manifest.skills}`);
500
+ }
501
+ const skillsContainment = relative(pluginRootReal, skillsRootReal);
502
+ const sepK = process.platform === 'win32' ? '\\' : '/';
503
+ if (
504
+ skillsContainment !== '' &&
505
+ (isAbsolute(skillsContainment) || skillsContainment === '..' || skillsContainment.startsWith(`..${sepK}`))
506
+ ) {
507
+ throw new Error(`codebuddy manifest skills "${manifest.skills}" escapes the plugin root after symlink resolution`);
508
+ }
509
+ entryAbs = resolve(skillsRootReal, entrySkill, 'SKILL.md');
510
+ }
511
+
512
+ // lstat the LEXICAL entry BEFORE realpath: a symlinked SKILL.md must be
513
+ // rejected outright (lstat-ing the resolved target would miss the symlink).
514
+ let entryLexicalStat;
515
+ try {
516
+ entryLexicalStat = await lstat(entryAbs);
517
+ } catch {
518
+ throw new Error(`codebuddy entry skill not found: ${relative(pluginRootReal, entryAbs) || 'SKILL.md'}`);
519
+ }
520
+ if (entryLexicalStat.isSymbolicLink()) {
521
+ throw new Error('codebuddy entry skill must not be a symlink');
522
+ }
523
+ if (!entryLexicalStat.isFile()) {
524
+ throw new Error('codebuddy entry skill is not a regular file');
525
+ }
526
+
527
+ const entryReal = await realpath(entryAbs).catch(() => null);
528
+ if (!entryReal) {
529
+ throw new Error(`codebuddy entry skill not found: ${relative(pluginRootReal, entryAbs) || 'SKILL.md'}`);
530
+ }
531
+ const entryContainment = relative(pluginRootReal, entryReal);
532
+ const sepE = process.platform === 'win32' ? '\\' : '/';
533
+ if (
534
+ entryContainment !== '' &&
535
+ (isAbsolute(entryContainment) || entryContainment === '..' || entryContainment.startsWith(`..${sepE}`))
536
+ ) {
537
+ throw new Error('codebuddy entry skill escapes the plugin root after symlink resolution');
538
+ }
539
+ return entryReal;
540
+ }
541
+
398
542
  const SUPPORTED_TYPES = [
399
543
  ActionType.PLUGIN_MANIFEST_VALIDATE,
400
544
  ActionType.PLUGIN_INSTALL_CHECK,
401
545
  ActionType.CLAUDE_MARKETPLACE_INSTALL,
402
546
  ActionType.CODEX_MARKETPLACE_INSTALL,
403
547
  ActionType.KIMI_MARKETPLACE_INSTALL,
548
+ ActionType.CODEBUDDY_MARKETPLACE_INSTALL,
404
549
  ];
405
550
 
406
551
  /** Safe repo pattern: owner/repo with alphanumeric, hyphens, dots, underscores. */
@@ -662,7 +807,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
662
807
  if (
663
808
  actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
664
809
  actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
665
- actionType === ActionType.KIMI_MARKETPLACE_INSTALL
810
+ actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
811
+ actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
666
812
  ) {
667
813
  // 1. Validate all parameters for injection safety
668
814
  const validation = validateMarketplaceParams(action);
@@ -752,12 +898,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
752
898
  });
753
899
  }
754
900
 
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).
901
+ // A non-automatable platform (kimi, codebuddy) has no trustworthy
902
+ // automated install: the whole repo is installed as one plugin. The
903
+ // authoritative manifest is read from the verified snapshot root via
904
+ // the platform strategy (kimi: kimi.plugin.json over
905
+ // .kimi-plugin/plugin.json; codebuddy: .codebuddy-plugin/plugin.json).
760
906
  if (!platform.automatable) {
907
+ // Error-message label keeps the kimi wording byte-identical while
908
+ // giving codebuddy its own wording.
909
+ const manifestLabel = consumer === 'codebuddy' ? 'codebuddy' : 'kimi';
761
910
  let kimiManifestResult;
762
911
  try {
763
912
  kimiManifestResult = await platform.strategy.readManifest(snapshotDirReal);
@@ -765,7 +914,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
765
914
  return createResult({
766
915
  actionType,
767
916
  status: ActionStatus.PREFLIGHT_FAILED,
768
- error: `frozen snapshot kimi manifest invalid: ${manifestErr.message}`,
917
+ error: `frozen snapshot ${manifestLabel} manifest invalid: ${manifestErr.message}`,
769
918
  });
770
919
  }
771
920
  const kimiManifest = kimiManifestResult.manifest;
@@ -787,6 +936,59 @@ export function createPluginMarketplaceAdapter(deps = {}) {
787
936
  }
788
937
 
789
938
  if (platform.automatable) {
939
+ if (action.marketplaceLocation === 'external') {
940
+ // External independent marketplace form: the marketplace index lives
941
+ // in the external repository (frozen by prepare), NOT in the unit
942
+ // snapshot, so the snapshot marketplace-manifest section is skipped
943
+ // entirely. The plugin manifest is read directly from the snapshot
944
+ // root and the frozen external identity fields are validated. ref was
945
+ // already injection-checked above (step 2); the entry skill reuses the
946
+ // automatable fixed layout validated below.
947
+ if (
948
+ typeof action.marketplaceCommitSha !== 'string'
949
+ || !/^[0-9a-f]{40}$/.test(action.marketplaceCommitSha)
950
+ ) {
951
+ return createResult({
952
+ actionType,
953
+ status: ActionStatus.PREFLIGHT_FAILED,
954
+ error: `external marketplace marketplaceCommitSha must be a 40-hex commit sha, got ${JSON.stringify(action.marketplaceCommitSha)}`,
955
+ });
956
+ }
957
+ if (
958
+ typeof action.repo !== 'string'
959
+ || !/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$/.test(action.repo)
960
+ ) {
961
+ return createResult({
962
+ actionType,
963
+ status: ActionStatus.PREFLIGHT_FAILED,
964
+ error: `external marketplace repo must be an owner/name repository, got ${JSON.stringify(action.repo)}`,
965
+ });
966
+ }
967
+ const externalManifestRelative = platform.manifestPaths.plugin;
968
+ const externalManifestPath = resolve(snapshotDirReal, externalManifestRelative);
969
+ const externalManifestResult = await validateManifestFile(externalManifestPath, ['name', 'version']);
970
+ if (!externalManifestResult.valid) {
971
+ return createResult({
972
+ actionType,
973
+ status: ActionStatus.PREFLIGHT_FAILED,
974
+ error: `frozen snapshot ${externalManifestRelative} invalid: ${externalManifestResult.error}`,
975
+ });
976
+ }
977
+ if (externalManifestResult.manifest.name !== action.plugin) {
978
+ return createResult({
979
+ actionType,
980
+ status: ActionStatus.PREFLIGHT_FAILED,
981
+ error: `plugin manifest name "${externalManifestResult.manifest.name}" does not match action plugin "${action.plugin}"`,
982
+ });
983
+ }
984
+ if (externalManifestResult.manifest.version !== action.version) {
985
+ return createResult({
986
+ actionType,
987
+ status: ActionStatus.PREFLIGHT_FAILED,
988
+ error: `plugin manifest version "${externalManifestResult.manifest.version}" does not match action version "${action.version}"`,
989
+ });
990
+ }
991
+ } else {
790
992
  // Verify marketplace files exist.
791
993
  // marketplace.json is at the snapshot root; plugin manifest is
792
994
  // resolved relative to the entry's declared source path.
@@ -923,17 +1125,21 @@ export function createPluginMarketplaceAdapter(deps = {}) {
923
1125
  });
924
1126
  }
925
1127
  }
1128
+ }
926
1129
 
927
1130
  // Verify the entry skill exists in the snapshot.
928
1131
  // Automatable platforms' manifests always declare ./skills/, so the
929
1132
  // 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.
1133
+ // them. A non-automatable platform (kimi, codebuddy) resolves the
1134
+ // entry skill via the manifest-declared skills root (MAJOR-4): the
1135
+ // root is validated + realpath-contained, and omitted `skills` means
1136
+ // the official single-skill root SKILL.md.
934
1137
  if (!platform.automatable) {
1138
+ const resolveEntrySkillFile = consumer === 'codebuddy'
1139
+ ? resolveCodeBuddyEntrySkillFile
1140
+ : resolveKimiEntrySkillFile;
935
1141
  try {
936
- await resolveKimiEntrySkillFile(snapshotDirReal, kimiSnapshotManifest, action.entrySkill);
1142
+ await resolveEntrySkillFile(snapshotDirReal, kimiSnapshotManifest, action.entrySkill);
937
1143
  } catch (entryErr) {
938
1144
  return createResult({
939
1145
  actionType,
@@ -1100,7 +1306,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1100
1306
  if (
1101
1307
  actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
1102
1308
  actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
1103
- actionType === ActionType.KIMI_MARKETPLACE_INSTALL
1309
+ actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
1310
+ actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
1104
1311
  ) {
1105
1312
  try {
1106
1313
  assertIsolatedConsumerWritesAuthorized(context, actionType);
@@ -1442,7 +1649,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1442
1649
  if (
1443
1650
  actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
1444
1651
  actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
1445
- actionType === ActionType.KIMI_MARKETPLACE_INSTALL
1652
+ actionType === ActionType.KIMI_MARKETPLACE_INSTALL ||
1653
+ actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL
1446
1654
  ) {
1447
1655
  const consumer = action.consumer;
1448
1656
  const runDir = context.runDir;
@@ -1493,7 +1701,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1493
1701
  // root (MAJOR-4), and manifest name/version.
1494
1702
  // Missing/expired/mismatched/escaping proof fails closed so a kimi
1495
1703
  // unit can never reach VERIFIED without it.
1496
- if (platform && !platform.automatable) {
1704
+ if (platform && !platform.automatable && actionType === ActionType.KIMI_MARKETPLACE_INSTALL) {
1497
1705
  const expectedRef = action.ref ?? `v${action.version}`;
1498
1706
 
1499
1707
  // Bind to the REAL frozen plan digest (A). Fail closed if the
@@ -1774,6 +1982,301 @@ export function createPluginMarketplaceAdapter(deps = {}) {
1774
1982
  });
1775
1983
  }
1776
1984
 
1985
+ // CodeBuddy protocol capability gap (parallel to the kimi branch
1986
+ // above; duplicated rather than abstracted so the kimi error strings,
1987
+ // file layout, and golden bytes stay byte-for-byte unchanged). The
1988
+ // codebuddy CLI cannot pin a frozen ref, so observe never execs a
1989
+ // codebuddy command. It consumes a structured human attestation bound
1990
+ // to the frozen plan digest + identity, validates the install path per
1991
+ // channel (desktop `~/.workbuddy` layout / isolated cli home), then
1992
+ // performs read-only verification of the installed copy: payload
1993
+ // digest vs the sealed authority, entry skill resolved via the
1994
+ // manifest skills root, and manifest name/version. Missing/expired/
1995
+ // mismatched/escaping proof fails closed so a codebuddy unit can never
1996
+ // reach VERIFIED without it.
1997
+ if (platform && !platform.automatable && actionType === ActionType.CODEBUDDY_MARKETPLACE_INSTALL) {
1998
+ const expectedRef = action.ref ?? `v${action.version}`;
1999
+
2000
+ // Bind to the REAL frozen plan digest (A). Fail closed if the
2001
+ // context does not carry an intact frozen plan.
2002
+ let boundPlanDigest;
2003
+ try {
2004
+ boundPlanDigest = await resolveCodeBuddyBoundPlanDigest(context);
2005
+ } catch (planErr) {
2006
+ return createResult({
2007
+ actionType,
2008
+ status: ActionStatus.OBSERVED,
2009
+ observation: {
2010
+ installed: false,
2011
+ error: `cannot bind codebuddy observation to the frozen plan: ${planErr.message}`,
2012
+ },
2013
+ });
2014
+ }
2015
+
2016
+ // Stable, cross-run attestation authority (B), keyed by the verified
2017
+ // plan digest + plugin, so it survives publish PARTIAL -> reconcile
2018
+ // -> verify (each uses a fresh runDir).
2019
+ let attestationDir;
2020
+ try {
2021
+ attestationDir = codebuddyAuthorityDir(context, boundPlanDigest, action.plugin);
2022
+ } catch (dirErr) {
2023
+ return createResult({
2024
+ actionType,
2025
+ status: ActionStatus.OBSERVED,
2026
+ observation: { installed: false, error: dirErr.message },
2027
+ });
2028
+ }
2029
+
2030
+ // The execute-emitted requirement must exist and bind to the action
2031
+ // and the frozen plan digest.
2032
+ let requirement = null;
2033
+ try {
2034
+ requirement = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_REQUIREMENT_FILE), 'utf8'));
2035
+ } catch {
2036
+ return createResult({
2037
+ actionType,
2038
+ status: ActionStatus.OBSERVED,
2039
+ observation: {
2040
+ installed: false,
2041
+ manualInstallRequired: true,
2042
+ error: 'codebuddy manual-install requirement is missing; run execute first',
2043
+ },
2044
+ });
2045
+ }
2046
+ if (
2047
+ requirement.planDigest !== boundPlanDigest ||
2048
+ requirement.plugin !== action.plugin ||
2049
+ requirement.version !== action.version ||
2050
+ requirement.entrySkill !== action.entrySkill ||
2051
+ requirement.repo !== action.repo ||
2052
+ requirement.ref !== expectedRef
2053
+ ) {
2054
+ return createResult({
2055
+ actionType,
2056
+ status: ActionStatus.OBSERVED,
2057
+ observation: {
2058
+ installed: false,
2059
+ error: 'codebuddy manual-install requirement does not match the frozen plan/action',
2060
+ },
2061
+ });
2062
+ }
2063
+
2064
+ // The trusted human attestation is mandatory and is read from the
2065
+ // stable authority dir. Without it the manual install has not been
2066
+ // proven: fail closed.
2067
+ let attestation = null;
2068
+ try {
2069
+ attestation = JSON.parse(await readFile(resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE), 'utf8'));
2070
+ } catch {
2071
+ return createResult({
2072
+ actionType,
2073
+ status: ActionStatus.OBSERVED,
2074
+ observation: {
2075
+ installed: false,
2076
+ manualInstallRequired: true,
2077
+ marketplaceSource: CODEBUDDY_MARKETPLACE_SOURCE,
2078
+ attestationDir,
2079
+ error: `codebuddy attestation is missing; write ${resolve(attestationDir, CODEBUDDY_ATTESTATION_FILE)} after the manual install (marketplace ${CODEBUDDY_MARKETPLACE_SOURCE})`,
2080
+ },
2081
+ });
2082
+ }
2083
+
2084
+ const attestationCheck = validateCodeBuddyAttestation(attestation, action, new Date().toISOString(), boundPlanDigest);
2085
+ if (!attestationCheck.valid) {
2086
+ return createResult({
2087
+ actionType,
2088
+ status: ActionStatus.OBSERVED,
2089
+ observation: { installed: false, error: attestationCheck.error },
2090
+ });
2091
+ }
2092
+
2093
+ // The attested install path must be a real directory (not a symlink)
2094
+ // that resolves successfully. Per-channel containment:
2095
+ // cli: installPath must resolve inside the ISOLATED, plan-digest
2096
+ // keyed codebuddy home's marketplace plugin root.
2097
+ // desktop: the segment-level `~/.workbuddy/...` layout check already
2098
+ // ran in validateCodeBuddyAttestation; here we only require
2099
+ // the path to resolve to a real directory.
2100
+ const installPath = resolve(attestation.installPath);
2101
+ let installPathStat;
2102
+ try {
2103
+ installPathStat = await lstat(installPath);
2104
+ } catch {
2105
+ return createResult({
2106
+ actionType,
2107
+ status: ActionStatus.OBSERVED,
2108
+ observation: { installed: false, error: `codebuddy install path does not exist: ${attestation.installPath}` },
2109
+ });
2110
+ }
2111
+ if (installPathStat.isSymbolicLink()) {
2112
+ return createResult({
2113
+ actionType,
2114
+ status: ActionStatus.OBSERVED,
2115
+ observation: { installed: false, error: `codebuddy install path must not be a symlink: ${attestation.installPath}` },
2116
+ });
2117
+ }
2118
+ if (!installPathStat.isDirectory()) {
2119
+ return createResult({
2120
+ actionType,
2121
+ status: ActionStatus.OBSERVED,
2122
+ observation: { installed: false, error: `codebuddy install path must be a directory: ${attestation.installPath}` },
2123
+ });
2124
+ }
2125
+ const installPathReal = await realpath(installPath).catch(() => null);
2126
+ if (!installPathReal) {
2127
+ return createResult({
2128
+ actionType,
2129
+ status: ActionStatus.OBSERVED,
2130
+ observation: { installed: false, error: `codebuddy install path cannot be resolved: ${attestation.installPath}` },
2131
+ });
2132
+ }
2133
+
2134
+ if (attestation.installChannel === 'cli') {
2135
+ const cliHome = codebuddyCliHome(attestationDir);
2136
+ const cliHomeReal = await realpath(cliHome).catch(() => null);
2137
+ if (!cliHomeReal) {
2138
+ return createResult({
2139
+ actionType,
2140
+ status: ActionStatus.OBSERVED,
2141
+ observation: {
2142
+ installed: false,
2143
+ error: `codebuddy isolated CLI home does not exist or cannot be resolved: ${cliHome}`,
2144
+ },
2145
+ });
2146
+ }
2147
+ const cliRootAbs = codebuddyCliInstallRoot(cliHomeReal, attestation.marketplace, action.plugin);
2148
+ const cliRootReal = await realpath(cliRootAbs).catch(() => null);
2149
+ if (!cliRootReal) {
2150
+ return createResult({
2151
+ actionType,
2152
+ status: ActionStatus.OBSERVED,
2153
+ observation: {
2154
+ installed: false,
2155
+ error: `codebuddy CLI marketplace plugin root does not exist: ${cliRootAbs}`,
2156
+ },
2157
+ });
2158
+ }
2159
+ const sepC = process.platform === 'win32' ? '\\' : '/';
2160
+ const relToRoot = relative(cliRootReal, installPathReal);
2161
+ if (
2162
+ relToRoot !== '' &&
2163
+ (isAbsolute(relToRoot) || relToRoot === '..' || relToRoot.startsWith(`..${sepC}`))
2164
+ ) {
2165
+ return createResult({
2166
+ actionType,
2167
+ status: ActionStatus.OBSERVED,
2168
+ observation: {
2169
+ installed: false,
2170
+ error: `codebuddy install path escapes the isolated CLI marketplace root (${cliRootReal}): ${attestation.installPath}`,
2171
+ },
2172
+ });
2173
+ }
2174
+ }
2175
+
2176
+ // Read-only payload binding: the installed copy must match the
2177
+ // sealed frozen authority exactly (transport-normalized).
2178
+ let manifestDigest;
2179
+ let payloadBinding = null;
2180
+ try {
2181
+ payloadBinding = await verifyInstalledMarketplacePayload(action, context, installPathReal, consumer);
2182
+ manifestDigest = payloadBinding.manifestDigest;
2183
+ } catch (digestErr) {
2184
+ return createResult({
2185
+ actionType,
2186
+ status: ActionStatus.OBSERVED,
2187
+ observation: {
2188
+ installed: true,
2189
+ installPath: installPathReal,
2190
+ error: `failed to bind installed codebuddy payload to frozen authority: ${digestErr.message}`,
2191
+ },
2192
+ });
2193
+ }
2194
+ if (manifestDigest !== action.manifestDigest) {
2195
+ return createResult({
2196
+ actionType,
2197
+ status: ActionStatus.OBSERVED,
2198
+ observation: {
2199
+ installed: true,
2200
+ installPath: installPathReal,
2201
+ error: 'installed codebuddy payload digest does not match the frozen plan digest',
2202
+ },
2203
+ });
2204
+ }
2205
+
2206
+ // Entry skill must resolve via the installed manifest's skills root,
2207
+ // with the codebuddy single-candidate manifest.
2208
+ let installedManifest;
2209
+ try {
2210
+ const readManifest = await platform.strategy.readManifest(installPathReal);
2211
+ installedManifest = readManifest.manifest;
2212
+ await resolveCodeBuddyEntrySkillFile(installPathReal, installedManifest, action.entrySkill);
2213
+ } catch (entryErr) {
2214
+ return createResult({
2215
+ actionType,
2216
+ status: ActionStatus.OBSERVED,
2217
+ observation: {
2218
+ installed: true,
2219
+ installPath: installPathReal,
2220
+ manifestDigest,
2221
+ entrySkillFound: false,
2222
+ error: `codebuddy entry skill not resolvable in installed copy: ${entryErr.message}`,
2223
+ },
2224
+ });
2225
+ }
2226
+
2227
+ // Installed manifest identity must match the frozen action.
2228
+ if (installedManifest.name !== action.plugin) {
2229
+ return createResult({
2230
+ actionType,
2231
+ status: ActionStatus.OBSERVED,
2232
+ observation: {
2233
+ installed: true,
2234
+ installPath: installPathReal,
2235
+ manifestDigest,
2236
+ entrySkillFound: true,
2237
+ error: `installed codebuddy manifest name "${installedManifest.name}" does not match action plugin "${action.plugin}"`,
2238
+ },
2239
+ });
2240
+ }
2241
+ if (installedManifest.version !== action.version) {
2242
+ return createResult({
2243
+ actionType,
2244
+ status: ActionStatus.OBSERVED,
2245
+ observation: {
2246
+ installed: true,
2247
+ installPath: installPathReal,
2248
+ manifestDigest,
2249
+ entrySkillFound: true,
2250
+ error: `installed codebuddy manifest version "${installedManifest.version}" does not match action version "${action.version}"`,
2251
+ },
2252
+ });
2253
+ }
2254
+
2255
+ // Build the observation from independently verified fields only.
2256
+ // marketplace + installChannel ARE validated codebuddy identity
2257
+ // fields (unlike kimi's MINOR-1 marketplace exclusion).
2258
+ const observation = {
2259
+ installed: true,
2260
+ installPath: installPathReal,
2261
+ entrySkillFound: true,
2262
+ entrySkill: action.entrySkill,
2263
+ manifestDigest,
2264
+ consumer,
2265
+ plugin: installedManifest.name,
2266
+ version: installedManifest.version,
2267
+ repo: action.repo,
2268
+ ref: expectedRef,
2269
+ marketplace: attestation.marketplace,
2270
+ installChannel: attestation.installChannel,
2271
+ ...extraInstalledPathsAudit(payloadBinding),
2272
+ };
2273
+ return createResult({
2274
+ actionType,
2275
+ status: ActionStatus.OBSERVED,
2276
+ observation,
2277
+ });
2278
+ }
2279
+
1777
2280
  // Read execute evidence — mandatory for observe validation
1778
2281
  let evidence = null;
1779
2282
  try {
@@ -2075,6 +2578,16 @@ export function createPluginMarketplaceAdapter(deps = {}) {
2075
2578
  if (evidence.repo) observation.repo = evidence.repo;
2076
2579
  if (evidence.ref) observation.ref = evidence.ref;
2077
2580
 
2581
+ // External independent marketplace form: bind the frozen external
2582
+ // identity markers so verify's expected subset matches. These are
2583
+ // frozen plan identity (like repo/ref), never re-derived from the
2584
+ // remote at observe time; the install-side entry observation above
2585
+ // (CLI list version binding) compensates for the weak name-freeze.
2586
+ if (action.marketplaceLocation === 'external') {
2587
+ observation.marketplaceLocation = action.marketplaceLocation;
2588
+ observation.marketplaceCommitSha = action.marketplaceCommitSha;
2589
+ }
2590
+
2078
2591
  return createResult({
2079
2592
  actionType,
2080
2593
  status: ActionStatus.OBSERVED,