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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +4 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/INSTALL.md +183 -21
- package/INSTALL.zh-CN.md +160 -16
- package/README.md +112 -13
- package/README.zh-CN.md +72 -11
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +2240 -587
- package/adapters/claude/schemas/release-plan.schema.json +44 -2
- package/adapters/claude/schemas/release-project.schema.json +46 -4
- package/adapters/claude/schemas/release-run.schema.json +1 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2240 -587
- package/adapters/codex/schemas/release-plan.schema.json +44 -2
- package/adapters/codex/schemas/release-project.schema.json +46 -4
- package/adapters/codex/schemas/release-run.schema.json +1 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2240 -587
- package/adapters/kimi/schemas/release-plan.schema.json +44 -2
- package/adapters/kimi/schemas/release-project.schema.json +46 -4
- package/adapters/kimi/schemas/release-run.schema.json +1 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +4 -2
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +2240 -587
- package/adapters/workbuddy/schemas/release-plan.schema.json +44 -2
- package/adapters/workbuddy/schemas/release-project.schema.json +46 -4
- package/adapters/workbuddy/schemas/release-run.schema.json +1 -0
- package/bin/release-skill-cli.mjs +46 -4
- package/bin/release-skill.bundle.mjs +2240 -587
- package/package.json +1 -1
- package/references/02-project-config.md +7 -0
- package/references/06-adapter-contract.md +21 -2
- package/schemas/release-plan.schema.json +44 -2
- package/schemas/release-project.schema.json +46 -4
- package/schemas/release-run.schema.json +1 -0
- package/scripts/sync-public-files.mjs +8 -4
- package/src/adapters/contract.mjs +1 -0
- package/src/adapters/plugin-marketplace.mjs +796 -41
- package/src/commands/assess.mjs +50 -1
- package/src/commands/prepare.mjs +342 -9
- package/src/commands/publish.mjs +1 -0
- package/src/commands/reconcile.mjs +1 -0
- package/src/commands/setup.mjs +7 -3
- package/src/commands/verify.mjs +13 -5
- package/src/core/baseline.mjs +13 -0
- package/src/core/checkpoints.mjs +7 -2
- package/src/core/plan.mjs +275 -4
- package/src/core/verification-gates.mjs +1 -1
- package/src/platforms/codebuddy.mjs +658 -0
- package/src/platforms/registry.mjs +258 -5
- 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 (
|
|
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 (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
//
|
|
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 (${
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
756
|
-
//
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
if (
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
1367
|
+
// Structured-cli platforms' manifests always declare ./skills/, so the
|
|
929
1368
|
// fixed skills/<entrySkill>/SKILL.md layout is authoritative for
|
|
930
|
-
// them. A
|
|
931
|
-
// via the manifest-declared skills root (MAJOR-4): the
|
|
932
|
-
// validated + realpath-contained, and omitted `skills` means
|
|
933
|
-
// official single-skill root SKILL.md.
|
|
934
|
-
if (
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1460
|
-
// kimi-shaped env
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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,
|