release-skill 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +58 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +52 -16
- package/README.zh-CN.md +46 -16
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +7 -7
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/claude/schemas/release-plan.schema.json +36 -0
- package/adapters/claude/schemas/release-project.schema.json +31 -0
- package/adapters/claude/skills/release-finish/SKILL.md +5 -5
- package/adapters/claude/skills/release-help/SKILL.md +11 -4
- package/adapters/claude/skills/release-prepare/SKILL.md +11 -3
- package/adapters/claude/skills/release-verify/SKILL.md +2 -1
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +7 -7
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/codex/schemas/release-plan.schema.json +36 -0
- package/adapters/codex/schemas/release-project.schema.json +31 -0
- package/adapters/codex/skills/release-finish/SKILL.md +5 -5
- package/adapters/codex/skills/release-help/SKILL.md +11 -4
- package/adapters/codex/skills/release-prepare/SKILL.md +11 -3
- package/adapters/codex/skills/release-verify/SKILL.md +2 -1
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +7 -7
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/kimi/schemas/release-plan.schema.json +36 -0
- package/adapters/kimi/schemas/release-project.schema.json +31 -0
- package/adapters/kimi/skills/release-finish/SKILL.md +5 -5
- package/adapters/kimi/skills/release-help/SKILL.md +11 -4
- package/adapters/kimi/skills/release-prepare/SKILL.md +11 -3
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -1
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +7 -7
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1513 -352
- package/adapters/workbuddy/schemas/release-plan.schema.json +36 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +31 -0
- package/adapters/workbuddy/skills/release-finish/SKILL.md +5 -5
- package/adapters/workbuddy/skills/release-help/SKILL.md +11 -4
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +11 -3
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -1
- package/bin/consumer-contract-vectors.json +7 -7
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/release-skill-cli.mjs +113 -22
- package/bin/release-skill.bundle.mjs +1513 -352
- package/package.json +4 -4
- package/platform-manifest.json +4 -4
- package/references/.render-manifest.json +4 -4
- package/references/01-state-machine.md +18 -0
- package/references/02-project-config.md +20 -0
- package/schemas/release-plan.schema.json +36 -0
- package/schemas/release-project.schema.json +31 -0
- package/skills/release-finish/SKILL.md +5 -5
- package/skills/release-help/SKILL.md +11 -4
- package/skills/release-prepare/SKILL.md +11 -3
- package/skills/release-verify/SKILL.md +2 -1
- package/skills-src/release-finish/SKILL.md +5 -5
- package/skills-src/release-help/SKILL.md +11 -4
- package/skills-src/release-prepare/SKILL.md +11 -3
- package/skills-src/release-verify/SKILL.md +2 -1
- package/src/commands/post-release-local.mjs +373 -26
- package/src/commands/prepare.mjs +361 -16
- package/src/commands/ship.mjs +190 -13
- package/src/commands/verify.mjs +175 -2
- package/src/core/foundation-plugin-verification.mjs +22 -2
- package/src/core/plan.mjs +45 -1
- package/src/platforms/registry.mjs +97 -0
- package/src/producers/foundation-resource-projection.mjs +3 -3
- package/src/readme/contract.mjs +23 -3
package/src/commands/ship.mjs
CHANGED
|
@@ -15,11 +15,92 @@ import {
|
|
|
15
15
|
} from './post-release-local.mjs';
|
|
16
16
|
import {
|
|
17
17
|
ReleaseError,
|
|
18
|
+
CONFIG_INVALID,
|
|
18
19
|
GATE_FAILED,
|
|
19
20
|
PLAN_DIGEST_MISMATCH,
|
|
20
21
|
CONSUMER_VERIFICATION_DEFERRED,
|
|
21
22
|
} from '../core/errors.mjs';
|
|
22
23
|
|
|
24
|
+
function normalizeNewShipUnitIds(releaseUnits, requestedUnitIds) {
|
|
25
|
+
if (requestedUnitIds === undefined) return undefined;
|
|
26
|
+
const availableUnitIds = (releaseUnits ?? []).map((unit) => unit.id);
|
|
27
|
+
if (!Array.isArray(requestedUnitIds) || requestedUnitIds.length === 0) {
|
|
28
|
+
throw new ReleaseError(
|
|
29
|
+
CONFIG_INVALID,
|
|
30
|
+
'explicit ship release-unit selection must contain at least one unit id',
|
|
31
|
+
{ requestedUnitIds, availableUnitIds },
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const requestedSet = new Set(requestedUnitIds);
|
|
35
|
+
const invalid = requestedUnitIds.filter((unitId) => typeof unitId !== 'string' || unitId.length === 0);
|
|
36
|
+
const unknown = requestedUnitIds.filter((unitId) => !availableUnitIds.includes(unitId));
|
|
37
|
+
if (invalid.length > 0 || requestedSet.size !== requestedUnitIds.length || unknown.length > 0) {
|
|
38
|
+
throw new ReleaseError(
|
|
39
|
+
CONFIG_INVALID,
|
|
40
|
+
'ship release-unit selection contains empty, duplicate, or unknown unit ids',
|
|
41
|
+
{ requestedUnitIds, availableUnitIds },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return availableUnitIds.filter((unitId) => requestedSet.has(unitId));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameUnitSelection(left, right) {
|
|
48
|
+
return Array.isArray(left)
|
|
49
|
+
&& Array.isArray(right)
|
|
50
|
+
&& left.length === right.length
|
|
51
|
+
&& new Set(left).size === left.length
|
|
52
|
+
&& new Set(right).size === right.length
|
|
53
|
+
&& left.every((unitId) => right.includes(unitId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sameUnitOrder(left, right) {
|
|
57
|
+
return Array.isArray(left)
|
|
58
|
+
&& Array.isArray(right)
|
|
59
|
+
&& left.length === right.length
|
|
60
|
+
&& left.every((unitId, index) => unitId === right[index]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unitScopeMismatch(statePath, stateUnitIds, requestedUnitIds, message) {
|
|
64
|
+
return new ReleaseError(
|
|
65
|
+
GATE_FAILED,
|
|
66
|
+
message,
|
|
67
|
+
{
|
|
68
|
+
reason: 'SHIP_STATE_UNIT_SCOPE_MISMATCH',
|
|
69
|
+
statePath,
|
|
70
|
+
stateUnitIds: stateUnitIds ?? null,
|
|
71
|
+
requestedUnitIds: requestedUnitIds ?? null,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertFrozenPlanUnitScope({ statePath, stateUnitIds, expectedUnitIds, plan }) {
|
|
77
|
+
const planUnitIds = (plan.units ?? []).map((unit) => unit.id);
|
|
78
|
+
const exactMatch = Array.isArray(expectedUnitIds)
|
|
79
|
+
&& planUnitIds.length === expectedUnitIds.length
|
|
80
|
+
&& planUnitIds.every((unitId, index) => unitId === expectedUnitIds[index]);
|
|
81
|
+
if (!exactMatch) {
|
|
82
|
+
throw unitScopeMismatch(
|
|
83
|
+
statePath,
|
|
84
|
+
stateUnitIds,
|
|
85
|
+
planUnitIds,
|
|
86
|
+
'ship state release-unit scope does not match frozen plan.units',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readFrozenPlanForUnitScope(planPath, statePath, stateUnitIds) {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(await readFile(planPath, 'utf8'));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw unitScopeMismatch(
|
|
96
|
+
statePath,
|
|
97
|
+
stateUnitIds,
|
|
98
|
+
null,
|
|
99
|
+
`cannot verify ship state release-unit scope against frozen plan: ${error.message}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
23
104
|
async function writeJsonAtomic(path, value) {
|
|
24
105
|
await mkdir(dirname(path), { recursive: true });
|
|
25
106
|
const { stateDigest: _oldDigest, ...body } = value;
|
|
@@ -93,7 +174,8 @@ async function defaultDependencies() {
|
|
|
93
174
|
};
|
|
94
175
|
}
|
|
95
176
|
|
|
96
|
-
function publicState(state) {
|
|
177
|
+
function publicState(state, { postRelease } = {}) {
|
|
178
|
+
const visiblePostRelease = postRelease === undefined ? state.postRelease : postRelease;
|
|
97
179
|
return {
|
|
98
180
|
command: 'ship',
|
|
99
181
|
status: state.status,
|
|
@@ -102,6 +184,7 @@ function publicState(state) {
|
|
|
102
184
|
...(state.hooks && state.hooks.length > 0 ? {
|
|
103
185
|
hooks: state.hooks,
|
|
104
186
|
} : {}),
|
|
187
|
+
...(state.selectedUnitIds ? { selectedUnitIds: state.selectedUnitIds } : {}),
|
|
105
188
|
...(state.planPath ? {
|
|
106
189
|
planPath: state.planPath,
|
|
107
190
|
planDigest: state.planDigest,
|
|
@@ -117,7 +200,7 @@ function publicState(state) {
|
|
|
117
200
|
...(state.requirements ? { requirements: state.requirements } : {}),
|
|
118
201
|
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
119
202
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
120
|
-
...(
|
|
203
|
+
...(visiblePostRelease ? { postRelease: visiblePostRelease } : {}),
|
|
121
204
|
externalActionsIncludedInPlanApproval: true,
|
|
122
205
|
verificationGatesIncludedInPlanApproval: true,
|
|
123
206
|
postPublishCheckpointApprovalsIncludedInPlanApproval: false,
|
|
@@ -334,6 +417,24 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
334
417
|
);
|
|
335
418
|
}
|
|
336
419
|
}
|
|
420
|
+
if (state && options.unitIds !== undefined) {
|
|
421
|
+
if (!Array.isArray(state.selectedUnitIds)) {
|
|
422
|
+
throw unitScopeMismatch(
|
|
423
|
+
statePath,
|
|
424
|
+
null,
|
|
425
|
+
options.unitIds,
|
|
426
|
+
'an existing full-scope or legacy ship state cannot acquire --unit; provide a new --state path',
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (!sameUnitSelection(state.selectedUnitIds, options.unitIds)) {
|
|
430
|
+
throw unitScopeMismatch(
|
|
431
|
+
statePath,
|
|
432
|
+
state.selectedUnitIds,
|
|
433
|
+
options.unitIds,
|
|
434
|
+
'ship state release-unit scope does not match the requested --unit values',
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
337
438
|
if (state?.gitTransport) {
|
|
338
439
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
|
|
339
440
|
}
|
|
@@ -341,6 +442,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
341
442
|
if (!state) {
|
|
342
443
|
const loaded = await deps.loadProjectConfig({ root });
|
|
343
444
|
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
445
|
+
const selectedUnitIds = normalizeNewShipUnitIds(loaded.config.releaseUnits, options.unitIds);
|
|
344
446
|
state = {
|
|
345
447
|
schemaVersion: 2,
|
|
346
448
|
root,
|
|
@@ -348,6 +450,7 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
348
450
|
targetVersion: options.targetVersion ?? null,
|
|
349
451
|
configDigest: loaded.configDigest,
|
|
350
452
|
hooks,
|
|
453
|
+
...(selectedUnitIds ? { selectedUnitIds } : {}),
|
|
351
454
|
status: 'NEW',
|
|
352
455
|
createdAt: new Date().toISOString(),
|
|
353
456
|
updatedAt: new Date().toISOString(),
|
|
@@ -364,8 +467,53 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
364
467
|
await writeJsonAtomic(statePath, state);
|
|
365
468
|
}
|
|
366
469
|
|
|
470
|
+
if (state.planPath && Array.isArray(state.selectedUnitIds)) {
|
|
471
|
+
const frozenPlan = await readFrozenPlanForUnitScope(
|
|
472
|
+
state.planPath,
|
|
473
|
+
statePath,
|
|
474
|
+
state.selectedUnitIds,
|
|
475
|
+
);
|
|
476
|
+
assertFrozenPlanUnitScope({
|
|
477
|
+
statePath,
|
|
478
|
+
stateUnitIds: state.selectedUnitIds,
|
|
479
|
+
expectedUnitIds: state.selectedUnitIds,
|
|
480
|
+
plan: frozenPlan,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
367
484
|
if (state.status === 'NEW') {
|
|
368
485
|
const loaded = await deps.loadProjectConfig({ root });
|
|
486
|
+
let stateChanged = false;
|
|
487
|
+
if (Array.isArray(state.selectedUnitIds)) {
|
|
488
|
+
const normalizedStateUnitIds = normalizeNewShipUnitIds(
|
|
489
|
+
loaded.config.releaseUnits,
|
|
490
|
+
state.selectedUnitIds,
|
|
491
|
+
);
|
|
492
|
+
if (!sameUnitSelection(normalizedStateUnitIds, state.selectedUnitIds)) {
|
|
493
|
+
throw unitScopeMismatch(
|
|
494
|
+
statePath,
|
|
495
|
+
state.selectedUnitIds,
|
|
496
|
+
normalizedStateUnitIds,
|
|
497
|
+
'ship state release-unit scope no longer matches project configuration',
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (!sameUnitOrder(normalizedStateUnitIds, state.selectedUnitIds)) {
|
|
501
|
+
if (state.planPath) {
|
|
502
|
+
throw unitScopeMismatch(
|
|
503
|
+
statePath,
|
|
504
|
+
state.selectedUnitIds,
|
|
505
|
+
normalizedStateUnitIds,
|
|
506
|
+
'frozen ship state release-unit order no longer matches project configuration',
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
state = {
|
|
510
|
+
...state,
|
|
511
|
+
selectedUnitIds: normalizedStateUnitIds,
|
|
512
|
+
updatedAt: new Date().toISOString(),
|
|
513
|
+
};
|
|
514
|
+
stateChanged = true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
369
517
|
if (loaded.configDigest !== state.configDigest) {
|
|
370
518
|
const hooks = Object.keys(loaded.config.hooks ?? {}).sort();
|
|
371
519
|
state = {
|
|
@@ -375,8 +523,9 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
375
523
|
status: 'NEW',
|
|
376
524
|
updatedAt: new Date().toISOString(),
|
|
377
525
|
};
|
|
378
|
-
|
|
526
|
+
stateChanged = true;
|
|
379
527
|
}
|
|
528
|
+
if (stateChanged) await writeJsonAtomic(statePath, state);
|
|
380
529
|
const prepared = await deps.prepareRelease({
|
|
381
530
|
root,
|
|
382
531
|
version: state.targetVersion ?? undefined,
|
|
@@ -385,10 +534,24 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
385
534
|
hooksAuthorized: true,
|
|
386
535
|
verificationGatesAuthorized: true,
|
|
387
536
|
hookCache: true,
|
|
537
|
+
...(state.selectedUnitIds ? { unitIds: state.selectedUnitIds } : {}),
|
|
388
538
|
});
|
|
539
|
+
let frozenPlan = null;
|
|
540
|
+
if (Array.isArray(state.selectedUnitIds) || deps.preflightGitTransports) {
|
|
541
|
+
frozenPlan = Array.isArray(state.selectedUnitIds)
|
|
542
|
+
? await readFrozenPlanForUnitScope(prepared.planPath, statePath, state.selectedUnitIds)
|
|
543
|
+
: JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
544
|
+
}
|
|
545
|
+
if (Array.isArray(state.selectedUnitIds)) {
|
|
546
|
+
assertFrozenPlanUnitScope({
|
|
547
|
+
statePath,
|
|
548
|
+
stateUnitIds: state.selectedUnitIds,
|
|
549
|
+
expectedUnitIds: state.selectedUnitIds,
|
|
550
|
+
plan: frozenPlan,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
389
553
|
let transportPreflight = null;
|
|
390
554
|
if (deps.preflightGitTransports) {
|
|
391
|
-
const frozenPlan = JSON.parse(await readFile(prepared.planPath, 'utf8'));
|
|
392
555
|
transportPreflight = await deps.preflightGitTransports(frozenPlan);
|
|
393
556
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
|
|
394
557
|
}
|
|
@@ -556,15 +719,6 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
556
719
|
updatedAt: new Date().toISOString(),
|
|
557
720
|
};
|
|
558
721
|
await writeJsonAtomic(statePath, state);
|
|
559
|
-
if (verified.status === 'VERIFIED') {
|
|
560
|
-
try {
|
|
561
|
-
state.postRelease = derivePostReleaseChecklist(plan);
|
|
562
|
-
} catch (error) {
|
|
563
|
-
state.postRelease = unavailablePostReleaseChecklist(plan, error);
|
|
564
|
-
}
|
|
565
|
-
state.updatedAt = new Date().toISOString();
|
|
566
|
-
await writeJsonAtomic(statePath, state);
|
|
567
|
-
}
|
|
568
722
|
} catch (error) {
|
|
569
723
|
if (error?.code !== CONSUMER_VERIFICATION_DEFERRED) throw error;
|
|
570
724
|
state = {
|
|
@@ -673,5 +827,28 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
673
827
|
}
|
|
674
828
|
}
|
|
675
829
|
|
|
830
|
+
if (state.status === 'VERIFIED') {
|
|
831
|
+
const finalPlan = JSON.parse(await readFile(state.planPath, 'utf8'));
|
|
832
|
+
const hasPostVerify = normalizePostPublishView(finalPlan).some((declaration) => (
|
|
833
|
+
(declaration.hooks ?? []).some((hook) => hook.phase === 'postVerify')
|
|
834
|
+
));
|
|
835
|
+
const localFinishRunPath = hasPostVerify
|
|
836
|
+
? (state.postVerify?.status === 'DISTRIBUTED' ? state.postVerify.runPath : undefined)
|
|
837
|
+
: state.verifyRunPath;
|
|
838
|
+
let postRelease;
|
|
839
|
+
try {
|
|
840
|
+
postRelease = derivePostReleaseChecklist(finalPlan, {
|
|
841
|
+
root,
|
|
842
|
+
statePath,
|
|
843
|
+
unitIds: state.selectedUnitIds,
|
|
844
|
+
runPath: localFinishRunPath,
|
|
845
|
+
postVerifyComplete: !hasPostVerify || state.postVerify?.status === 'DISTRIBUTED',
|
|
846
|
+
});
|
|
847
|
+
} catch (error) {
|
|
848
|
+
postRelease = unavailablePostReleaseChecklist(finalPlan, error);
|
|
849
|
+
}
|
|
850
|
+
return publicState(state, { postRelease });
|
|
851
|
+
}
|
|
852
|
+
|
|
676
853
|
return publicState(state);
|
|
677
854
|
}
|
package/src/commands/verify.mjs
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
verifyFrozenPluginWithFoundation,
|
|
69
69
|
} from '../core/foundation-plugin-verification.mjs';
|
|
70
70
|
import { resolveUnitScopedPath } from '../snapshot/public-path.mjs';
|
|
71
|
+
import { canonicalJson } from '../core/digest.mjs';
|
|
71
72
|
import {
|
|
72
73
|
normalizeRegistry,
|
|
73
74
|
registryTokenKey,
|
|
@@ -83,7 +84,7 @@ import {
|
|
|
83
84
|
} from '../core/skill-resource-closure.mjs';
|
|
84
85
|
import { deriveSurfaceHostBinding, pluginRootFromManifestRelativePath } from '../core/surface-host-bindings.mjs';
|
|
85
86
|
import { isRemoteWriteAction, isMarketplaceAction } from '../core/checkpoints.mjs';
|
|
86
|
-
import { PLATFORMS, normalizeHostId } from '../platforms/registry.mjs';
|
|
87
|
+
import { PLATFORMS, normalizeHostId, projectObservedStandaloneIndexInstallIdentity } from '../platforms/registry.mjs';
|
|
87
88
|
import {
|
|
88
89
|
shouldSkipVerification,
|
|
89
90
|
INSTALLATION_CONTRACT_ALGORITHM_VERSION,
|
|
@@ -183,6 +184,140 @@ function defaultClock() {
|
|
|
183
184
|
return new Date().toISOString();
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
const FIRST_RELEASE_BOOTSTRAP_MODE = 'manual-index-checkpoint';
|
|
188
|
+
const EXTERNAL_MARKETPLACE_SHA_RE = /^[0-9a-f]{40}$/;
|
|
189
|
+
|
|
190
|
+
function parseMarketplaceHead(stdout) {
|
|
191
|
+
if (typeof stdout !== 'string') return null;
|
|
192
|
+
let sha = null;
|
|
193
|
+
let defaultBranch = null;
|
|
194
|
+
for (const line of stdout.trim().split('\n').filter(Boolean)) {
|
|
195
|
+
const tab = line.indexOf('\t');
|
|
196
|
+
if (tab < 0) continue;
|
|
197
|
+
const left = line.slice(0, tab);
|
|
198
|
+
const right = line.slice(tab + 1);
|
|
199
|
+
if (right !== 'HEAD') continue;
|
|
200
|
+
if (left.startsWith('ref: refs/heads/')) defaultBranch = left.slice('ref: refs/heads/'.length);
|
|
201
|
+
else if (EXTERNAL_MARKETPLACE_SHA_RE.test(left)) sha = left;
|
|
202
|
+
}
|
|
203
|
+
return sha && defaultBranch ? { sha, defaultBranch } : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function defaultObserveMarketplaceHead(repo, { githubHost = 'github.com' } = {}) {
|
|
207
|
+
try {
|
|
208
|
+
const { stdout } = await execFile('git', [
|
|
209
|
+
'ls-remote', '--symref', `https://${githubHost}/${repo}.git`, 'HEAD',
|
|
210
|
+
], { shell: false, encoding: 'utf8', timeout: 30000 });
|
|
211
|
+
const parsed = parseMarketplaceHead(stdout);
|
|
212
|
+
return parsed ? { status: 'observed', ...parsed } : { status: 'unknown', error: 'could not resolve marketplace HEAD' };
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return { status: 'unknown', error: error.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function decodeMarketplaceIndex(content) {
|
|
219
|
+
if (typeof content !== 'string') return null;
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(Buffer.from(content.replace(/\s/g, ''), 'base64').toString('utf8'));
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function defaultFetchMarketplaceIndex(repo, path, ref, { githubHost = 'github.com' } = {}) {
|
|
228
|
+
try {
|
|
229
|
+
const { stdout } = await execFile('gh', [
|
|
230
|
+
'api', `repos/${repo}/contents/${path}?ref=${ref}`, '--jq', '.content',
|
|
231
|
+
], {
|
|
232
|
+
shell: false,
|
|
233
|
+
encoding: 'utf8',
|
|
234
|
+
timeout: 30000,
|
|
235
|
+
env: { ...process.env, GH_HOST: githubHost },
|
|
236
|
+
});
|
|
237
|
+
const index = decodeMarketplaceIndex(stdout);
|
|
238
|
+
return index && typeof index === 'object'
|
|
239
|
+
? { status: 'fetched', index }
|
|
240
|
+
: { status: 'unknown', error: 'could not decode marketplace index' };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return { status: 'unknown', error: error.message };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function verifyFirstReleaseBootstrapIndexes({
|
|
247
|
+
actions,
|
|
248
|
+
plan,
|
|
249
|
+
evidence,
|
|
250
|
+
observeHeadFn,
|
|
251
|
+
fetchIndexFn,
|
|
252
|
+
}) {
|
|
253
|
+
const bindings = new Map();
|
|
254
|
+
for (const action of actions) {
|
|
255
|
+
if (action.parameters?.firstReleaseBootstrap !== FIRST_RELEASE_BOOTSTRAP_MODE) continue;
|
|
256
|
+
const params = action.parameters;
|
|
257
|
+
const githubHost = params.githubHost ?? 'github.com';
|
|
258
|
+
const failDeferred = async (reason, observed = null) => {
|
|
259
|
+
await evidence.append({
|
|
260
|
+
phase: 'verify-marketplace-bootstrap-index',
|
|
261
|
+
actionId: action.id,
|
|
262
|
+
actionType: action.type,
|
|
263
|
+
status: 'needs-input',
|
|
264
|
+
reason,
|
|
265
|
+
...(observed ? { observed } : {}),
|
|
266
|
+
});
|
|
267
|
+
throw new ReleaseError(
|
|
268
|
+
CONSUMER_VERIFICATION_DEFERRED,
|
|
269
|
+
`first-release bootstrap marketplace index is not an exact match for action "${action.id}"; complete the manual index checkpoint and retry verify with the same plan`,
|
|
270
|
+
{ actionId: action.id, reason, observed, nextState: 'NEEDS_MANUAL_ATTESTATIONS' },
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
const observed = await observeHeadFn(params.repo, { githubHost });
|
|
274
|
+
if (observed?.status !== 'observed' || !EXTERNAL_MARKETPLACE_SHA_RE.test(observed.sha ?? '') || !observed.defaultBranch) {
|
|
275
|
+
await failDeferred(`could not observe current marketplace HEAD: ${observed?.error ?? 'unknown'}`, observed);
|
|
276
|
+
}
|
|
277
|
+
const path = params.marketplaceIndexPath;
|
|
278
|
+
const fetched = await fetchIndexFn(params.repo, path, observed.sha, { githubHost });
|
|
279
|
+
if (fetched?.status !== 'fetched' || !fetched.index || typeof fetched.index !== 'object') {
|
|
280
|
+
await failDeferred(`could not read marketplace index at ${observed.sha}: ${fetched?.error ?? 'unknown'}`, observed);
|
|
281
|
+
}
|
|
282
|
+
const index = fetched.index;
|
|
283
|
+
if (index.name !== params.marketplaceName) {
|
|
284
|
+
await failDeferred(`marketplace name mismatch: expected ${params.marketplaceName}, got ${index.name}`, { ...observed, name: index.name });
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(index.plugins)) {
|
|
287
|
+
await failDeferred('marketplace index plugins must be an array', { ...observed, pluginsType: typeof index.plugins });
|
|
288
|
+
}
|
|
289
|
+
const entries = index.plugins.filter((entry) => entry && entry.name === params.plugin);
|
|
290
|
+
let projectedEntry = null;
|
|
291
|
+
if (entries.length === 1) {
|
|
292
|
+
try {
|
|
293
|
+
projectedEntry = projectObservedStandaloneIndexInstallIdentity(params.consumer, entries[0]);
|
|
294
|
+
} catch {
|
|
295
|
+
projectedEntry = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (entries.length !== 1 || !projectedEntry || canonicalJson(projectedEntry) !== canonicalJson(params.selectedEntry)) {
|
|
299
|
+
await failDeferred('marketplace selectedEntry is missing, duplicated, or does not exactly match the plan-bound expected entry', {
|
|
300
|
+
...observed,
|
|
301
|
+
entryCount: entries.length,
|
|
302
|
+
selectedEntry: projectedEntry ?? entries[0] ?? null,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
bindings.set(action.id, {
|
|
306
|
+
marketplaceIndexSha: observed.sha,
|
|
307
|
+
marketplaceIndexRef: action.type === 'claude-marketplace-install' ? observed.defaultBranch : observed.sha,
|
|
308
|
+
});
|
|
309
|
+
await evidence.append({
|
|
310
|
+
phase: 'verify-marketplace-bootstrap-index',
|
|
311
|
+
actionId: action.id,
|
|
312
|
+
actionType: action.type,
|
|
313
|
+
status: 'observed',
|
|
314
|
+
marketplaceIndexSha: observed.sha,
|
|
315
|
+
marketplaceIndexRef: action.type === 'claude-marketplace-install' ? observed.defaultBranch : observed.sha,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
return bindings;
|
|
319
|
+
}
|
|
320
|
+
|
|
186
321
|
/**
|
|
187
322
|
* 验证摘要格式是否为合法的 64 位十六进制字符串。
|
|
188
323
|
*
|
|
@@ -957,6 +1092,8 @@ export async function verifyRelease(options) {
|
|
|
957
1092
|
execFn,
|
|
958
1093
|
configPath: configPathOpt,
|
|
959
1094
|
runPluginVerificationFn,
|
|
1095
|
+
observeFirstReleaseMarketplaceHeadFn,
|
|
1096
|
+
fetchFirstReleaseMarketplaceIndexFn,
|
|
960
1097
|
} = options ?? {};
|
|
961
1098
|
|
|
962
1099
|
const clockFn = typeof clockOpt === 'function' ? clockOpt : defaultClock;
|
|
@@ -1265,6 +1402,19 @@ export async function verifyRelease(options) {
|
|
|
1265
1402
|
const consumerVerificationReceipts = [];
|
|
1266
1403
|
const actions = plan.externalActions ?? [];
|
|
1267
1404
|
|
|
1405
|
+
// A bootstrap plan intentionally leaves the final marketplace index SHA
|
|
1406
|
+
// pending. Before any marketplace adapter call, observe the current
|
|
1407
|
+
// remote index and require an exact match for the plan-bound entry. A
|
|
1408
|
+
// mismatch is the existing deferred/manual-attestation state; no add or
|
|
1409
|
+
// install command is reached on this path.
|
|
1410
|
+
const bootstrapIndexBindings = await verifyFirstReleaseBootstrapIndexes({
|
|
1411
|
+
actions,
|
|
1412
|
+
plan,
|
|
1413
|
+
evidence,
|
|
1414
|
+
observeHeadFn: observeFirstReleaseMarketplaceHeadFn ?? defaultObserveMarketplaceHead,
|
|
1415
|
+
fetchIndexFn: fetchFirstReleaseMarketplaceIndexFn ?? defaultFetchMarketplaceIndex,
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1268
1418
|
// --- 自动发现可信消费端验证收据 ---
|
|
1269
1419
|
// 收据选择逻辑(per-action 从所有候选中选最新匹配):
|
|
1270
1420
|
// - 只读取同一权威 .release-skill/runs 的真实直接子目录
|
|
@@ -1533,10 +1683,21 @@ export async function verifyRelease(options) {
|
|
|
1533
1683
|
runDir,
|
|
1534
1684
|
};
|
|
1535
1685
|
|
|
1686
|
+
const bootstrapBinding = bootstrapIndexBindings.get(action.id) ?? null;
|
|
1536
1687
|
const actionInput = {
|
|
1537
1688
|
actionType: adapterActionType,
|
|
1538
1689
|
...action.parameters,
|
|
1539
1690
|
};
|
|
1691
|
+
if (bootstrapBinding) {
|
|
1692
|
+
actionInput.marketplaceCommitSha = bootstrapBinding.marketplaceIndexSha;
|
|
1693
|
+
actionInput.marketplaceIndexSha = bootstrapBinding.marketplaceIndexSha;
|
|
1694
|
+
actionInput.ref = bootstrapBinding.marketplaceIndexRef;
|
|
1695
|
+
actionInput.sourceDescriptor = {
|
|
1696
|
+
...actionInput.sourceDescriptor,
|
|
1697
|
+
marketplaceCommitSha: bootstrapBinding.marketplaceIndexSha,
|
|
1698
|
+
ref: bootstrapBinding.marketplaceIndexRef,
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1540
1701
|
|
|
1541
1702
|
// --- 安装契约摘要免验检查 ---
|
|
1542
1703
|
// 从计划中读取冻结的安装契约摘要。
|
|
@@ -1614,6 +1775,7 @@ export async function verifyRelease(options) {
|
|
|
1614
1775
|
actionType: action.type,
|
|
1615
1776
|
status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
|
|
1616
1777
|
installationContractDigest: currentDigest,
|
|
1778
|
+
...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
|
|
1617
1779
|
reason: '安装契约摘要未变化,跳过验证',
|
|
1618
1780
|
});
|
|
1619
1781
|
|
|
@@ -1634,6 +1796,7 @@ export async function verifyRelease(options) {
|
|
|
1634
1796
|
actionType: action.type,
|
|
1635
1797
|
status: VERIFICATION_RESOLVED_TYPES.NOT_REQUIRED_UNCHANGED,
|
|
1636
1798
|
installationContractDigest: currentDigest,
|
|
1799
|
+
...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
|
|
1637
1800
|
});
|
|
1638
1801
|
|
|
1639
1802
|
return;
|
|
@@ -1657,8 +1820,16 @@ export async function verifyRelease(options) {
|
|
|
1657
1820
|
}
|
|
1658
1821
|
|
|
1659
1822
|
// Step 3c: Verify (observe + match against plan expected state)
|
|
1823
|
+
const expectedInput = bootstrapBinding
|
|
1824
|
+
? {
|
|
1825
|
+
...action.expected,
|
|
1826
|
+
marketplaceCommitSha: bootstrapBinding.marketplaceIndexSha,
|
|
1827
|
+
marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha,
|
|
1828
|
+
ref: bootstrapBinding.marketplaceIndexRef,
|
|
1829
|
+
}
|
|
1830
|
+
: action.expected;
|
|
1660
1831
|
const verifyResult = await adapter.verify(
|
|
1661
|
-
{ ...actionInput, expected:
|
|
1832
|
+
{ ...actionInput, expected: expectedInput },
|
|
1662
1833
|
marketplaceContext,
|
|
1663
1834
|
);
|
|
1664
1835
|
|
|
@@ -1676,6 +1847,7 @@ export async function verifyRelease(options) {
|
|
|
1676
1847
|
status: resolvedStatus,
|
|
1677
1848
|
observation: verifyResult.observation,
|
|
1678
1849
|
error: verifyResult.error,
|
|
1850
|
+
...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
|
|
1679
1851
|
...(dist?.installationContractDigest ? { installationContractDigest: dist.installationContractDigest } : {}),
|
|
1680
1852
|
};
|
|
1681
1853
|
adapterChecks.push(check);
|
|
@@ -1700,6 +1872,7 @@ export async function verifyRelease(options) {
|
|
|
1700
1872
|
actionId: action.id,
|
|
1701
1873
|
actionType: action.type,
|
|
1702
1874
|
status: check.status,
|
|
1875
|
+
...(bootstrapBinding ? { marketplaceIndexSha: bootstrapBinding.marketplaceIndexSha, marketplaceIndexRef: bootstrapBinding.marketplaceIndexRef } : {}),
|
|
1703
1876
|
});
|
|
1704
1877
|
|
|
1705
1878
|
if (check.status === 'FAILED') {
|
|
@@ -33,7 +33,6 @@ import { ReleaseError, POST_PUBLISH_VERIFY_FAILED } from './errors.mjs';
|
|
|
33
33
|
// this value so an upstream contract change fails closed during adoption.
|
|
34
34
|
const FOUNDATION_PLUGIN_DRIVER_VERSION = '1.0.0';
|
|
35
35
|
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
36
|
-
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
|
|
37
36
|
|
|
38
37
|
function isSafeRelativePath(value) {
|
|
39
38
|
return typeof value === 'string'
|
|
@@ -46,7 +45,28 @@ function isSafeRelativePath(value) {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
function decodeCanonicalBase64(value) {
|
|
49
|
-
if (typeof value !== 'string' ||
|
|
48
|
+
if (typeof value !== 'string' || value.length % 4 !== 0) return null;
|
|
49
|
+
let paddingStart = -1;
|
|
50
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
51
|
+
const code = value.charCodeAt(index);
|
|
52
|
+
if (code === 61) {
|
|
53
|
+
if (paddingStart === -1) paddingStart = index;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (paddingStart !== -1 || !(
|
|
57
|
+
code >= 65 && code <= 90
|
|
58
|
+
|| code >= 97 && code <= 122
|
|
59
|
+
|| code >= 48 && code <= 57
|
|
60
|
+
|| code === 43
|
|
61
|
+
|| code === 47
|
|
62
|
+
)) return null;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
paddingStart !== -1
|
|
66
|
+
&& value.length - paddingStart > 2
|
|
67
|
+
|| paddingStart !== -1
|
|
68
|
+
&& paddingStart % 4 < 2
|
|
69
|
+
) return null;
|
|
50
70
|
const decoded = Buffer.from(value, 'base64');
|
|
51
71
|
return decoded.toString('base64') === value ? decoded : null;
|
|
52
72
|
}
|
package/src/core/plan.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { digestReleaseAssetIdentities, normalizeReleaseAssets } from './release-
|
|
|
34
34
|
// function bodies (after the import graph has settled), never at plan.mjs
|
|
35
35
|
// module-init time — a top-level read would hit the registry's
|
|
36
36
|
// not-yet-initialized bindings when the cycle is entered registry-first.
|
|
37
|
-
import { PLATFORMS } from '../platforms/registry.mjs';
|
|
37
|
+
import { PLATFORMS, buildExpectedStandaloneIndexInstallIdentity } from '../platforms/registry.mjs';
|
|
38
38
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
// Schema loaded from the authoritative JSON file (single source of truth)
|
|
@@ -1278,6 +1278,7 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
1278
1278
|
// 也不得接受"用 null 表示存在"的方案。
|
|
1279
1279
|
if (production && externalMarketplace && dist.marketplaceSourceType === 'standalone-index') {
|
|
1280
1280
|
const params = action.parameters;
|
|
1281
|
+
const bootstrap = dist.firstReleaseBootstrap === 'manual-index-checkpoint';
|
|
1281
1282
|
if (params) {
|
|
1282
1283
|
if (!params.marketplaceIndexPath) {
|
|
1283
1284
|
failures.push(
|
|
@@ -1294,6 +1295,41 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
1294
1295
|
`unit "${unitId}", action "${action.id}": parameters.selectedEntry is required for production standalone-index`,
|
|
1295
1296
|
);
|
|
1296
1297
|
}
|
|
1298
|
+
if (bootstrap) {
|
|
1299
|
+
if (params.firstReleaseBootstrap !== 'manual-index-checkpoint') {
|
|
1300
|
+
failures.push(
|
|
1301
|
+
`unit "${unitId}", action "${action.id}": parameters.firstReleaseBootstrap must be manual-index-checkpoint for a bootstrap distribution`,
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
if (params.marketplaceIndexSha !== null) {
|
|
1305
|
+
failures.push(
|
|
1306
|
+
`unit "${unitId}", action "${action.id}": parameters.marketplaceIndexSha must remain null until the manual index checkpoint`,
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
let expectedEntry = null;
|
|
1310
|
+
try {
|
|
1311
|
+
expectedEntry = buildExpectedStandaloneIndexInstallIdentity(platform, {
|
|
1312
|
+
name: plugin,
|
|
1313
|
+
version: targetVersion,
|
|
1314
|
+
repo: publicRepo,
|
|
1315
|
+
tag: expectedTag,
|
|
1316
|
+
sha: frozen?.commit,
|
|
1317
|
+
});
|
|
1318
|
+
} catch {
|
|
1319
|
+
// Missing frozen identity is reported by the surrounding
|
|
1320
|
+
// production-field checks; do not turn completeness into an
|
|
1321
|
+
// uncaught implementation error.
|
|
1322
|
+
}
|
|
1323
|
+
if (!expectedEntry || canonicalJson(params.selectedEntry) !== canonicalJson(expectedEntry)) {
|
|
1324
|
+
failures.push(
|
|
1325
|
+
`unit "${unitId}", action "${action.id}": bootstrap selectedEntry does not match the frozen plugin repository, tag, commit, and version`,
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
} else if (params.firstReleaseBootstrap !== undefined || params.marketplaceIndexSha !== undefined) {
|
|
1329
|
+
failures.push(
|
|
1330
|
+
`unit "${unitId}", action "${action.id}": bootstrap-only fields are not allowed without firstReleaseBootstrap`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1297
1333
|
}
|
|
1298
1334
|
}
|
|
1299
1335
|
|
|
@@ -1329,6 +1365,14 @@ export function validatePlanActionCompleteness(plan, options = {}) {
|
|
|
1329
1365
|
_checkRequired(action, 'expected.ref', action.expected?.ref, action.parameters?.ref, unitId, failures);
|
|
1330
1366
|
_checkRequired(action, 'expected.marketplaceLocation', action.expected?.marketplaceLocation, 'external', unitId, failures);
|
|
1331
1367
|
_checkRequired(action, 'expected.marketplaceCommitSha', action.expected?.marketplaceCommitSha, action.parameters?.marketplaceCommitSha, unitId, failures);
|
|
1368
|
+
if (dist.firstReleaseBootstrap === 'manual-index-checkpoint') {
|
|
1369
|
+
_checkRequired(action, 'expected.firstReleaseBootstrap', action.expected?.firstReleaseBootstrap, 'manual-index-checkpoint', unitId, failures);
|
|
1370
|
+
if (action.expected?.marketplaceIndexSha !== null) {
|
|
1371
|
+
failures.push(
|
|
1372
|
+
`unit "${unitId}", action "${action.id}": expected.marketplaceIndexSha must be null until the manual index checkpoint`,
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1332
1376
|
} else {
|
|
1333
1377
|
_checkRequired(action, 'expected.repo', action.expected?.repo, publicRepo, unitId, failures);
|
|
1334
1378
|
_checkRequired(action, 'expected.ref', action.expected?.ref, expectedTag, unitId, failures);
|