@kungfu-tech/buildchain 2.12.9-alpha.1 → 2.13.0-alpha.1
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/actions/promote-buildchain-ref/README.md +13 -0
- package/actions/run-lifecycle/README.md +5 -0
- package/bin/buildchain.mjs +62 -0
- package/dist/site/buildchain-contract.json +50 -25
- package/dist/site/buildchain-site.json +82 -41
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +24 -0
- package/dist/site/controller-registry.json +24 -4
- package/dist/site/kfd-claims.json +78 -16
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -10
- package/dist/site/node-api-registry.json +20 -7
- package/dist/site/page-registry.json +68 -27
- package/dist/site/public-surface-audit.json +65 -15
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +14 -14
- package/dist/site/workflow-registry.json +25 -9
- package/docs/MAP.md +1 -0
- package/docs/artifact-verification-envelope.md +113 -0
- package/docs/cli.md +22 -0
- package/docs/install.md +1 -3
- package/docs/publication-authority.md +26 -0
- package/docs/publish-transaction.md +1 -1
- package/docs/release-candidate.md +24 -0
- package/docs/release-governance.md +12 -6
- package/docs/release-propagation.md +3 -0
- package/docs/reusable-build-surface.md +13 -0
- package/docs/versioning.md +1 -0
- package/package.json +2 -1
- package/packages/core/README.md +13 -0
- package/packages/core/artifact-passport.js +6 -1
- package/packages/core/artifact-verification-envelope.js +386 -0
- package/packages/core/buildchain-config.js +6 -2
- package/packages/core/controller-evidence.js +2 -3
- package/packages/core/index.js +16 -0
- package/packages/core/publication-authority.js +281 -12
- package/packages/core/release-line-bootstrap.js +24 -0
- package/scripts/assemble-publication-artifact-admission.mjs +10 -0
- package/scripts/assemble-self-publication-admission.mjs +10 -1
- package/scripts/check-inventory.mjs +5 -0
- package/scripts/generate-site-bundle.mjs +4 -0
- package/scripts/release-candidate-resolver.mjs +52 -29
- package/scripts/run-lifecycle-core.mjs +50 -6
package/packages/core/index.js
CHANGED
|
@@ -138,19 +138,25 @@ export {
|
|
|
138
138
|
PUBLICATION_AUTHORITY_CLASSES,
|
|
139
139
|
PUBLICATION_AUTHORITY_REGISTRY_CONTRACT,
|
|
140
140
|
PUBLICATION_CAPABILITY_CONTRACT,
|
|
141
|
+
CONSUMER_PUBLICATION_DECISION_CONTRACT,
|
|
141
142
|
PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT,
|
|
142
143
|
PUBLICATION_GATE_DECISION_CONTRACT,
|
|
144
|
+
PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT,
|
|
143
145
|
RUNNER_PROVENANCE_CLASSES,
|
|
144
146
|
RUNNER_PROVENANCE_CONTRACT,
|
|
145
147
|
createPublicationAuthorityRegistry,
|
|
146
148
|
createPublicationAdmission,
|
|
147
149
|
createPublicationArtifactManifestSet,
|
|
150
|
+
createConsumerPublicationDecision,
|
|
148
151
|
createPublicationControlPlaneAudit,
|
|
149
152
|
createPublicationGateDecision,
|
|
153
|
+
createPublicationQualificationReceipt,
|
|
150
154
|
createRunnerProvenance,
|
|
151
155
|
detectPublicationAuthoritySignals,
|
|
152
156
|
publicationAuthorityDigest,
|
|
157
|
+
publicationGateAggregateBindings,
|
|
153
158
|
verifyPublicationAdmission,
|
|
159
|
+
verifyPublicationQualificationReceipt,
|
|
154
160
|
} from "./publication-authority.js";
|
|
155
161
|
|
|
156
162
|
export {
|
|
@@ -179,6 +185,16 @@ export {
|
|
|
179
185
|
verifyArtifactPassport,
|
|
180
186
|
} from "./artifact-passport.js";
|
|
181
187
|
|
|
188
|
+
export {
|
|
189
|
+
ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
|
|
190
|
+
ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT,
|
|
191
|
+
KFX_ADMISSION_INPUTS_CONTRACT,
|
|
192
|
+
artifactVerificationEnvelopeDigest,
|
|
193
|
+
projectArtifactVerificationEnvelopeToKfx,
|
|
194
|
+
sealArtifactVerificationReport,
|
|
195
|
+
verifyArtifactVerificationEnvelope,
|
|
196
|
+
} from "./artifact-verification-envelope.js";
|
|
197
|
+
|
|
182
198
|
export {
|
|
183
199
|
BUILDCHAIN_CONTRACT_LOCK,
|
|
184
200
|
BUILDCHAIN_RUNTIME_CONTRACT_WORLD,
|
|
@@ -16,6 +16,10 @@ export const PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT =
|
|
|
16
16
|
"kungfu-buildchain-publication-control-plane-audit";
|
|
17
17
|
export const PUBLICATION_GATE_DECISION_CONTRACT =
|
|
18
18
|
"kungfu-buildchain-publication-gate-decision";
|
|
19
|
+
export const CONSUMER_PUBLICATION_DECISION_CONTRACT =
|
|
20
|
+
"kungfu-buildchain-consumer-publication-decision";
|
|
21
|
+
export const PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT =
|
|
22
|
+
"kungfu-buildchain-publication-qualification-receipt";
|
|
19
23
|
export const PUBLICATION_ARTIFACT_MANIFEST_SET_CONTRACT =
|
|
20
24
|
"kungfu-buildchain-publication-artifact-manifest-set";
|
|
21
25
|
|
|
@@ -225,6 +229,16 @@ export function createPublicationControlPlaneAudit({
|
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
export function createPublicationAdmission(input = {}) {
|
|
232
|
+
const qualificationRequired = input.qualification?.required === true;
|
|
233
|
+
const qualification = {
|
|
234
|
+
required: qualificationRequired,
|
|
235
|
+
predicateId: qualificationRequired
|
|
236
|
+
? requiredString(input.qualification?.predicateId, "qualification.predicateId")
|
|
237
|
+
: "",
|
|
238
|
+
predicateDigest: qualificationRequired
|
|
239
|
+
? normalizeDigest(input.qualification?.predicateDigest, "qualification.predicateDigest")
|
|
240
|
+
: "",
|
|
241
|
+
};
|
|
228
242
|
const payload = {
|
|
229
243
|
schemaVersion: 1,
|
|
230
244
|
contract: PUBLICATION_ADMISSION_CONTRACT,
|
|
@@ -236,6 +250,10 @@ export function createPublicationAdmission(input = {}) {
|
|
|
236
250
|
runtimeSha: normalizeGitSha(input.runtimeSha, "runtimeSha"),
|
|
237
251
|
contractDigest: normalizeDigest(input.contractDigest, "contractDigest"),
|
|
238
252
|
policyDigest: normalizeDigest(input.policyDigest, "policyDigest"),
|
|
253
|
+
gateRegistryDigest: normalizeDigest(
|
|
254
|
+
input.gateRegistryDigest || input.policyDigest,
|
|
255
|
+
"gateRegistryDigest",
|
|
256
|
+
),
|
|
239
257
|
controllerReceiptDigest: normalizeDigest(input.controllerReceiptDigest, "controllerReceiptDigest"),
|
|
240
258
|
runnerProvenanceDigest: normalizeDigest(input.runnerProvenanceDigest, "runnerProvenanceDigest"),
|
|
241
259
|
controlPlaneAuditDigest: normalizeDigest(input.controlPlaneAuditDigest, "controlPlaneAuditDigest"),
|
|
@@ -249,6 +267,7 @@ export function createPublicationAdmission(input = {}) {
|
|
|
249
267
|
nonce: requiredString(input.nonce, "nonce"),
|
|
250
268
|
issuedAt: new Date(parseTime(input.issuedAt, "issuedAt")).toISOString(),
|
|
251
269
|
expiresAt: new Date(parseTime(input.expiresAt, "expiresAt")).toISOString(),
|
|
270
|
+
qualification,
|
|
252
271
|
};
|
|
253
272
|
return { ...payload, admissionDigest: publicationAuthorityDigest(payload) };
|
|
254
273
|
}
|
|
@@ -381,8 +400,7 @@ export function createPublicationArtifactManifestSet({
|
|
|
381
400
|
return { ...payload, manifestSetDigest: publicationAuthorityDigest(payload) };
|
|
382
401
|
}
|
|
383
402
|
|
|
384
|
-
function
|
|
385
|
-
const passportSourceSha = passport.source?.headSha || passport.source?.sha;
|
|
403
|
+
export function publicationGateAggregateBindings(gateAggregate) {
|
|
386
404
|
if (gateAggregate?.contract === "buildchain.shifu-gate-aggregate/v1") {
|
|
387
405
|
const { digest, ...payload } = gateAggregate;
|
|
388
406
|
const actualDigest = sha256Json(payload);
|
|
@@ -392,12 +410,17 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
392
410
|
if (gateAggregate.status !== "pass" || gateAggregate.qualifying !== true) {
|
|
393
411
|
throw new Error("gate aggregate is not qualifying");
|
|
394
412
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
const registryDigest = normalizeDigest(
|
|
414
|
+
gateAggregate.registry?.digest,
|
|
415
|
+
"gateAggregate.registry.digest",
|
|
416
|
+
);
|
|
399
417
|
const policyDigest = normalizeDigest(gateAggregate.matrixDigest, "gateAggregate.matrixDigest");
|
|
400
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
gateAggregateDigest: actualDigest,
|
|
420
|
+
policyDigest,
|
|
421
|
+
registryDigest,
|
|
422
|
+
sourceSha: normalizeGitSha(gateAggregate.sourceSha, "gateAggregate.sourceSha"),
|
|
423
|
+
};
|
|
401
424
|
}
|
|
402
425
|
if (gateAggregate?.contract !== PUBLICATION_GATE_DECISION_CONTRACT) {
|
|
403
426
|
throw new Error("gate evidence must be a Shifu Gate aggregate or explicit publication Gate decision");
|
|
@@ -410,9 +433,6 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
410
433
|
if (gateAggregate.required !== false) {
|
|
411
434
|
throw new Error("a required Gate policy must supply a qualifying Shifu Gate aggregate");
|
|
412
435
|
}
|
|
413
|
-
if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
|
|
414
|
-
throw new Error("publication Gate decision source SHA mismatch");
|
|
415
|
-
}
|
|
416
436
|
const normalizedPolicy = {
|
|
417
437
|
profile: requiredString(gateAggregate.profile, "gateDecision.profile"),
|
|
418
438
|
required: false,
|
|
@@ -423,7 +443,21 @@ function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
|
423
443
|
if (normalizeDigest(gateAggregate.policyDigest, "gateDecision.policyDigest") !== policyDigest) {
|
|
424
444
|
throw new Error("publication Gate policy digest mismatch");
|
|
425
445
|
}
|
|
426
|
-
return {
|
|
446
|
+
return {
|
|
447
|
+
gateAggregateDigest: actualDigest,
|
|
448
|
+
policyDigest,
|
|
449
|
+
registryDigest: policyDigest,
|
|
450
|
+
sourceSha: normalizeGitSha(gateAggregate.sourceSha, "gateDecision.sourceSha"),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function validateGateAggregate(gateAggregate, { admission, passport }) {
|
|
455
|
+
const bindings = publicationGateAggregateBindings(gateAggregate);
|
|
456
|
+
const passportSourceSha = passport.source?.headSha || passport.source?.sha;
|
|
457
|
+
if (![admission.sourceSha, passportSourceSha].includes(bindings.sourceSha)) {
|
|
458
|
+
throw new Error("gate aggregate source SHA mismatch");
|
|
459
|
+
}
|
|
460
|
+
return bindings;
|
|
427
461
|
}
|
|
428
462
|
|
|
429
463
|
function validatePublicationEvidence(publicationEvidence, admission) {
|
|
@@ -458,6 +492,7 @@ function validatePublicationEvidence(publicationEvidence, admission) {
|
|
|
458
492
|
contractDigest,
|
|
459
493
|
gateAggregateDigest: gate.gateAggregateDigest,
|
|
460
494
|
policyDigest: gate.policyDigest,
|
|
495
|
+
gateRegistryDigest: gate.registryDigest,
|
|
461
496
|
artifactDigest: candidate.candidateDigest,
|
|
462
497
|
evidenceDigest: publicationAuthorityDigest({
|
|
463
498
|
sourceTreeSha: candidate.sourceTreeSha,
|
|
@@ -551,6 +586,7 @@ function validatePublicationEvidence(publicationEvidence, admission) {
|
|
|
551
586
|
contractDigest,
|
|
552
587
|
gateAggregateDigest: gate.gateAggregateDigest,
|
|
553
588
|
policyDigest: gate.policyDigest,
|
|
589
|
+
gateRegistryDigest: gate.registryDigest,
|
|
554
590
|
artifactDigest: manifestSet.manifestSetDigest,
|
|
555
591
|
evidenceDigest: publicationAuthorityDigest({
|
|
556
592
|
passportCandidateHash: passport.candidateHash,
|
|
@@ -712,7 +748,7 @@ export function verifyPublicationAdmission({
|
|
|
712
748
|
for (const key of ["sourceSha", "runtimeSha"]) {
|
|
713
749
|
normalizeGitSha(admission[key], `admission.${key}`);
|
|
714
750
|
}
|
|
715
|
-
for (const key of ["contractDigest", "policyDigest", "controllerReceiptDigest", "gateAggregateDigest", "artifactDigest"]) {
|
|
751
|
+
for (const key of ["contractDigest", "policyDigest", "controllerReceiptDigest", "gateAggregateDigest", "artifactDigest", ...(admission.gateRegistryDigest ? ["gateRegistryDigest"] : [])]) {
|
|
716
752
|
normalizeDigest(admission[key], `admission.${key}`);
|
|
717
753
|
}
|
|
718
754
|
|
|
@@ -726,6 +762,13 @@ export function verifyPublicationAdmission({
|
|
|
726
762
|
expectedDigest: admission.controlPlaneAuditDigest,
|
|
727
763
|
});
|
|
728
764
|
const evidence = validatePublicationEvidence(publicationEvidence, admission);
|
|
765
|
+
if (
|
|
766
|
+
admission.gateRegistryDigest &&
|
|
767
|
+
normalizeDigest(admission.gateRegistryDigest, "admission.gateRegistryDigest") !==
|
|
768
|
+
evidence.gateRegistryDigest
|
|
769
|
+
) {
|
|
770
|
+
throw new Error("Gate registry evidence binding mismatch");
|
|
771
|
+
}
|
|
729
772
|
const { admissionDigest: suppliedAdmissionDigest, producerDecision: _ignored, ...admissionPayload } = admission;
|
|
730
773
|
const actualAdmissionDigest = publicationAuthorityDigest(admissionPayload);
|
|
731
774
|
if (normalizeDigest(suppliedAdmissionDigest, "admission.admissionDigest") !== actualAdmissionDigest) {
|
|
@@ -744,6 +787,8 @@ export function verifyPublicationAdmission({
|
|
|
744
787
|
runtimeSha: normalizeGitSha(admission.runtimeSha, "admission.runtimeSha"),
|
|
745
788
|
contractDigest: normalizeDigest(admission.contractDigest, "admission.contractDigest"),
|
|
746
789
|
policyDigest: normalizeDigest(admission.policyDigest, "admission.policyDigest"),
|
|
790
|
+
authorityRegistryDigest: actualRegistryDigest,
|
|
791
|
+
gateRegistryDigest: evidence.gateRegistryDigest,
|
|
747
792
|
controllerReceiptDigest: normalizeDigest(admission.controllerReceiptDigest, "admission.controllerReceiptDigest"),
|
|
748
793
|
product: admission.product,
|
|
749
794
|
target: admission.target,
|
|
@@ -758,7 +803,231 @@ export function verifyPublicationAdmission({
|
|
|
758
803
|
gateAggregateDigest: normalizeDigest(admission.gateAggregateDigest, "admission.gateAggregateDigest"),
|
|
759
804
|
environment,
|
|
760
805
|
nonce,
|
|
806
|
+
issuedAt: admission.issuedAt,
|
|
761
807
|
expiresAt: admission.expiresAt,
|
|
808
|
+
qualification: admission.qualification?.required === true
|
|
809
|
+
? {
|
|
810
|
+
required: true,
|
|
811
|
+
predicateId: requiredString(
|
|
812
|
+
admission.qualification.predicateId,
|
|
813
|
+
"admission.qualification.predicateId",
|
|
814
|
+
),
|
|
815
|
+
predicateDigest: normalizeDigest(
|
|
816
|
+
admission.qualification.predicateDigest,
|
|
817
|
+
"admission.qualification.predicateDigest",
|
|
818
|
+
),
|
|
819
|
+
}
|
|
820
|
+
: { required: false, predicateId: "", predicateDigest: "" },
|
|
762
821
|
};
|
|
763
822
|
return { ...capability, capabilityDigest: publicationAuthorityDigest(capability) };
|
|
764
823
|
}
|
|
824
|
+
|
|
825
|
+
function validatePublicationCapability(capability, now = new Date()) {
|
|
826
|
+
if (capability?.contract !== PUBLICATION_CAPABILITY_CONTRACT) {
|
|
827
|
+
throw new Error("publication capability contract mismatch");
|
|
828
|
+
}
|
|
829
|
+
const { capabilityDigest: supplied, ...payload } = capability;
|
|
830
|
+
const actual = publicationAuthorityDigest(payload);
|
|
831
|
+
if (normalizeDigest(supplied, "capability.capabilityDigest") !== actual) {
|
|
832
|
+
throw new Error("publication capability digest mismatch");
|
|
833
|
+
}
|
|
834
|
+
if (capability.decision !== "allow") {
|
|
835
|
+
throw new Error("publication capability did not allow provider mutation");
|
|
836
|
+
}
|
|
837
|
+
const nowMs = now instanceof Date ? now.getTime() : parseTime(now, "now");
|
|
838
|
+
if (parseTime(capability.expiresAt, "capability.expiresAt") <= nowMs) {
|
|
839
|
+
throw new Error("publication capability is stale");
|
|
840
|
+
}
|
|
841
|
+
return actual;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function validateCapabilityGateAggregate(capability, gateAggregate) {
|
|
845
|
+
const gate = publicationGateAggregateBindings(gateAggregate);
|
|
846
|
+
if (gate.gateAggregateDigest !== normalizeDigest(capability.gateAggregateDigest, "capability.gateAggregateDigest")) {
|
|
847
|
+
throw new Error("publication qualification Gate aggregate binding mismatch");
|
|
848
|
+
}
|
|
849
|
+
if (gate.policyDigest !== normalizeDigest(capability.policyDigest, "capability.policyDigest")) {
|
|
850
|
+
throw new Error("publication qualification Gate policy binding mismatch");
|
|
851
|
+
}
|
|
852
|
+
if (gate.registryDigest !== normalizeDigest(capability.gateRegistryDigest, "capability.gateRegistryDigest")) {
|
|
853
|
+
throw new Error("publication qualification Gate registry binding mismatch");
|
|
854
|
+
}
|
|
855
|
+
if (gate.sourceSha !== normalizeGitSha(capability.sourceSha, "capability.sourceSha")) {
|
|
856
|
+
throw new Error("publication qualification Gate source binding mismatch");
|
|
857
|
+
}
|
|
858
|
+
return gate;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export function createConsumerPublicationDecision({
|
|
862
|
+
capability,
|
|
863
|
+
gateAggregate,
|
|
864
|
+
decision,
|
|
865
|
+
predicateId,
|
|
866
|
+
predicateDigest,
|
|
867
|
+
evidence = {},
|
|
868
|
+
now = new Date(),
|
|
869
|
+
} = {}) {
|
|
870
|
+
const capabilityDigest = validatePublicationCapability(capability, now);
|
|
871
|
+
const gate = validateCapabilityGateAggregate(capability, gateAggregate);
|
|
872
|
+
if (capability.qualification?.required !== true) {
|
|
873
|
+
throw new Error("publication capability did not require consumer qualification");
|
|
874
|
+
}
|
|
875
|
+
const normalizedPredicateId = requiredString(predicateId, "predicateId");
|
|
876
|
+
const normalizedPredicateDigest = normalizeDigest(predicateDigest, "predicateDigest");
|
|
877
|
+
if (normalizedPredicateId !== capability.qualification.predicateId) {
|
|
878
|
+
throw new Error("consumer publication predicate id mismatch");
|
|
879
|
+
}
|
|
880
|
+
if (normalizedPredicateDigest !== capability.qualification.predicateDigest) {
|
|
881
|
+
throw new Error("consumer publication predicate digest mismatch");
|
|
882
|
+
}
|
|
883
|
+
const normalizedDecision = requiredString(decision, "decision");
|
|
884
|
+
if (!["allow", "deny"].includes(normalizedDecision)) {
|
|
885
|
+
throw new Error("consumer publication decision must be allow or deny");
|
|
886
|
+
}
|
|
887
|
+
const payload = {
|
|
888
|
+
schemaVersion: 1,
|
|
889
|
+
contract: CONSUMER_PUBLICATION_DECISION_CONTRACT,
|
|
890
|
+
decision: normalizedDecision,
|
|
891
|
+
predicateId: normalizedPredicateId,
|
|
892
|
+
predicateDigest: normalizedPredicateDigest,
|
|
893
|
+
capabilityDigest,
|
|
894
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
895
|
+
sourceSha: capability.sourceSha,
|
|
896
|
+
artifactDigest: capability.artifactDigest,
|
|
897
|
+
version: capability.version,
|
|
898
|
+
channel: capability.channel,
|
|
899
|
+
nonce: capability.nonce,
|
|
900
|
+
evidence,
|
|
901
|
+
};
|
|
902
|
+
return { ...payload, decisionDigest: publicationAuthorityDigest(payload) };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function createPublicationQualificationReceipt({
|
|
906
|
+
capability,
|
|
907
|
+
gateAggregate,
|
|
908
|
+
consumerDecision,
|
|
909
|
+
now = new Date(),
|
|
910
|
+
} = {}) {
|
|
911
|
+
const capabilityDigest = validatePublicationCapability(capability, now);
|
|
912
|
+
const gate = validateCapabilityGateAggregate(capability, gateAggregate);
|
|
913
|
+
if (capability.qualification?.required !== true) {
|
|
914
|
+
throw new Error("publication capability did not require consumer qualification");
|
|
915
|
+
}
|
|
916
|
+
if (consumerDecision?.contract !== CONSUMER_PUBLICATION_DECISION_CONTRACT) {
|
|
917
|
+
throw new Error("consumer publication decision contract mismatch");
|
|
918
|
+
}
|
|
919
|
+
const { decisionDigest: suppliedDecisionDigest, ...decisionPayload } = consumerDecision;
|
|
920
|
+
const decisionDigest = publicationAuthorityDigest(decisionPayload);
|
|
921
|
+
if (normalizeDigest(suppliedDecisionDigest, "consumerDecision.decisionDigest") !== decisionDigest) {
|
|
922
|
+
throw new Error("consumer publication decision digest mismatch");
|
|
923
|
+
}
|
|
924
|
+
if (consumerDecision.decision !== "allow") {
|
|
925
|
+
throw new Error("consumer publication predicate did not qualify");
|
|
926
|
+
}
|
|
927
|
+
const exactBindings = {
|
|
928
|
+
predicateId: capability.qualification.predicateId,
|
|
929
|
+
predicateDigest: capability.qualification.predicateDigest,
|
|
930
|
+
capabilityDigest,
|
|
931
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
932
|
+
sourceSha: capability.sourceSha,
|
|
933
|
+
artifactDigest: capability.artifactDigest,
|
|
934
|
+
version: capability.version,
|
|
935
|
+
channel: capability.channel,
|
|
936
|
+
nonce: capability.nonce,
|
|
937
|
+
};
|
|
938
|
+
for (const [key, expected] of Object.entries(exactBindings)) {
|
|
939
|
+
if (String(consumerDecision[key] || "") !== String(expected)) {
|
|
940
|
+
throw new Error(`consumer publication decision ${key} binding mismatch`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const payload = {
|
|
944
|
+
schemaVersion: 1,
|
|
945
|
+
contract: PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT,
|
|
946
|
+
decision: "allow",
|
|
947
|
+
...exactBindings,
|
|
948
|
+
authorityRegistryDigest: capability.authorityRegistryDigest,
|
|
949
|
+
gateRegistryDigest: gate.registryDigest,
|
|
950
|
+
policyDigest: gate.policyDigest,
|
|
951
|
+
runtimeSha: capability.runtimeSha,
|
|
952
|
+
contractDigest: capability.contractDigest,
|
|
953
|
+
controllerReceiptDigest: capability.controllerReceiptDigest,
|
|
954
|
+
runnerProvenanceDigest: capability.runnerProvenanceDigest,
|
|
955
|
+
controlPlaneAuditDigest: capability.controlPlaneAuditDigest,
|
|
956
|
+
publicationEvidenceDigest: capability.publicationEvidenceDigest,
|
|
957
|
+
product: capability.product,
|
|
958
|
+
target: capability.target,
|
|
959
|
+
issuedAt: capability.issuedAt,
|
|
960
|
+
expiresAt: capability.expiresAt,
|
|
961
|
+
consumerDecisionDigest: decisionDigest,
|
|
962
|
+
};
|
|
963
|
+
return { ...payload, receiptDigest: publicationAuthorityDigest(payload) };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function verifyPublicationQualificationReceipt({
|
|
967
|
+
receipt,
|
|
968
|
+
capability,
|
|
969
|
+
gateAggregate,
|
|
970
|
+
expected = {},
|
|
971
|
+
usedNonces = [],
|
|
972
|
+
now = new Date(),
|
|
973
|
+
} = {}) {
|
|
974
|
+
const capabilityDigest = validatePublicationCapability(capability, now);
|
|
975
|
+
const gate = validateCapabilityGateAggregate(capability, gateAggregate);
|
|
976
|
+
if (capability.qualification?.required !== true) {
|
|
977
|
+
throw new Error("publication capability did not require consumer qualification");
|
|
978
|
+
}
|
|
979
|
+
if (receipt?.contract !== PUBLICATION_QUALIFICATION_RECEIPT_CONTRACT) {
|
|
980
|
+
throw new Error("publication qualification receipt contract mismatch");
|
|
981
|
+
}
|
|
982
|
+
if (receipt.decision !== "allow") {
|
|
983
|
+
throw new Error("publication qualification receipt did not allow provider mutation");
|
|
984
|
+
}
|
|
985
|
+
const exactBindings = {
|
|
986
|
+
predicateId: capability.qualification.predicateId,
|
|
987
|
+
predicateDigest: capability.qualification.predicateDigest,
|
|
988
|
+
capabilityDigest,
|
|
989
|
+
gateAggregateDigest: gate.gateAggregateDigest,
|
|
990
|
+
authorityRegistryDigest: capability.authorityRegistryDigest,
|
|
991
|
+
gateRegistryDigest: gate.registryDigest,
|
|
992
|
+
policyDigest: gate.policyDigest,
|
|
993
|
+
sourceSha: capability.sourceSha,
|
|
994
|
+
runtimeSha: capability.runtimeSha,
|
|
995
|
+
contractDigest: capability.contractDigest,
|
|
996
|
+
controllerReceiptDigest: capability.controllerReceiptDigest,
|
|
997
|
+
runnerProvenanceDigest: capability.runnerProvenanceDigest,
|
|
998
|
+
controlPlaneAuditDigest: capability.controlPlaneAuditDigest,
|
|
999
|
+
publicationEvidenceDigest: capability.publicationEvidenceDigest,
|
|
1000
|
+
artifactDigest: capability.artifactDigest,
|
|
1001
|
+
product: capability.product,
|
|
1002
|
+
target: capability.target,
|
|
1003
|
+
version: capability.version,
|
|
1004
|
+
channel: capability.channel,
|
|
1005
|
+
nonce: capability.nonce,
|
|
1006
|
+
issuedAt: capability.issuedAt,
|
|
1007
|
+
expiresAt: capability.expiresAt,
|
|
1008
|
+
};
|
|
1009
|
+
for (const [key, value] of Object.entries(exactBindings)) {
|
|
1010
|
+
if (String(receipt[key] || "") !== String(value)) {
|
|
1011
|
+
throw new Error(`publication qualification receipt ${key} binding mismatch`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
normalizeDigest(
|
|
1015
|
+
receipt.consumerDecisionDigest,
|
|
1016
|
+
"receipt.consumerDecisionDigest",
|
|
1017
|
+
);
|
|
1018
|
+
const { receiptDigest: suppliedReceiptDigest, ...receiptPayload } = receipt;
|
|
1019
|
+
const receiptDigest = publicationAuthorityDigest(receiptPayload);
|
|
1020
|
+
if (normalizeDigest(suppliedReceiptDigest, "receipt.receiptDigest") !== receiptDigest) {
|
|
1021
|
+
throw new Error("publication qualification receipt digest mismatch");
|
|
1022
|
+
}
|
|
1023
|
+
const nonce = requiredString(receipt.nonce, "receipt.nonce");
|
|
1024
|
+
if (new Set(usedNonces.map(String)).has(nonce)) {
|
|
1025
|
+
throw new Error("publication qualification receipt nonce was replayed");
|
|
1026
|
+
}
|
|
1027
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
1028
|
+
if (value !== undefined && String(receipt[key] || "") !== String(value)) {
|
|
1029
|
+
throw new Error(`publication qualification receipt ${key} binding mismatch`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return { ok: true, receiptDigest, nonce };
|
|
1033
|
+
}
|
|
@@ -65,6 +65,29 @@ function writeJson(value) {
|
|
|
65
65
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function updateReleaseImpactVersionState(cwd, plan) {
|
|
69
|
+
const filePath = path.join(cwd, ".buildchain", "release-impact.json");
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const current = readJsonIfExists(filePath);
|
|
74
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
75
|
+
throw new Error(".buildchain/release-impact.json must be a JSON object");
|
|
76
|
+
}
|
|
77
|
+
const next = {
|
|
78
|
+
...current,
|
|
79
|
+
release: {
|
|
80
|
+
...(current.release && typeof current.release === "object" ? current.release : {}),
|
|
81
|
+
version: plan.initialVersion,
|
|
82
|
+
line: plan.line,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const content = writeJson(next);
|
|
86
|
+
if (content !== writeJson(current)) {
|
|
87
|
+
fs.writeFileSync(filePath, content);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
function discoverVersionStateFiles(cwd, loadedConfig) {
|
|
69
92
|
if (loadedConfig?.config?.version?.files?.length) {
|
|
70
93
|
return discoverConfiguredVersionStateFiles(cwd, loadedConfig);
|
|
@@ -212,6 +235,7 @@ export function writeReleaseLineBootstrapVersionState({
|
|
|
212
235
|
for (const file of changed) {
|
|
213
236
|
fs.writeFileSync(path.join(cwd, file.path), file.content);
|
|
214
237
|
}
|
|
238
|
+
updateReleaseImpactVersionState(cwd, plan);
|
|
215
239
|
const lifecycleVersionState =
|
|
216
240
|
getLifecycleStage(loadedConfig, "version-state") ||
|
|
217
241
|
getLifecycleStage(loadedConfig, "version_state");
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
createPublicationAdmission,
|
|
9
9
|
createPublicationGateDecision,
|
|
10
10
|
createRunnerProvenance,
|
|
11
|
+
publicationGateAggregateBindings,
|
|
11
12
|
} from "../packages/core/publication-authority.js";
|
|
12
13
|
import { buildPublicationArtifactCandidate } from "./publication-artifact-candidate.mjs";
|
|
13
14
|
|
|
@@ -126,6 +127,8 @@ async function main() {
|
|
|
126
127
|
isolation: "github-hosted-single-job",
|
|
127
128
|
});
|
|
128
129
|
const issuedAt = new Date();
|
|
130
|
+
const gateBindings = publicationGateAggregateBindings(gateAggregate);
|
|
131
|
+
const qualificationRequired = process.env.BUILDCHAIN_CONSUMER_QUALIFICATION_REQUIRED === "true";
|
|
129
132
|
const admission = createPublicationAdmission({
|
|
130
133
|
registryDigest: registry.registryDigest,
|
|
131
134
|
workflowPath:
|
|
@@ -137,6 +140,7 @@ async function main() {
|
|
|
137
140
|
runtimeSha,
|
|
138
141
|
contractDigest: controllerReceipt.runtime?.contractDigest,
|
|
139
142
|
policyDigest: gateAggregate.policyDigest,
|
|
143
|
+
gateRegistryDigest: gateBindings.registryDigest,
|
|
140
144
|
controllerReceiptDigest: controllerReceipt.digest,
|
|
141
145
|
runnerProvenanceDigest: runnerProvenance.receiptDigest,
|
|
142
146
|
controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
|
|
@@ -150,6 +154,11 @@ async function main() {
|
|
|
150
154
|
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}:paper`,
|
|
151
155
|
issuedAt: issuedAt.toISOString(),
|
|
152
156
|
expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
|
|
157
|
+
qualification: {
|
|
158
|
+
required: qualificationRequired,
|
|
159
|
+
predicateId: process.env.BUILDCHAIN_CONSUMER_PREDICATE_ID || "",
|
|
160
|
+
predicateDigest: process.env.BUILDCHAIN_CONSUMER_PREDICATE_DIGEST || "",
|
|
161
|
+
},
|
|
153
162
|
});
|
|
154
163
|
const bindingNames = [
|
|
155
164
|
"repository",
|
|
@@ -158,6 +167,7 @@ async function main() {
|
|
|
158
167
|
"runtimeSha",
|
|
159
168
|
"contractDigest",
|
|
160
169
|
"policyDigest",
|
|
170
|
+
"gateRegistryDigest",
|
|
161
171
|
"controllerReceiptDigest",
|
|
162
172
|
"gateAggregateDigest",
|
|
163
173
|
"environment",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createPublicationArtifactManifestSet,
|
|
10
10
|
createPublicationGateDecision,
|
|
11
11
|
createRunnerProvenance,
|
|
12
|
+
publicationGateAggregateBindings,
|
|
12
13
|
} from "../packages/core/publication-authority.js";
|
|
13
14
|
|
|
14
15
|
function required(name) {
|
|
@@ -148,6 +149,8 @@ async function main() {
|
|
|
148
149
|
const packageJson = JSON.parse(fs.readFileSync(path.join(runtimeRoot, "package.json"), "utf8"));
|
|
149
150
|
const publicationVersion = required("BUILDCHAIN_PUBLICATION_VERSION");
|
|
150
151
|
const issuedAt = new Date();
|
|
152
|
+
const gateBindings = publicationGateAggregateBindings(gateAggregate);
|
|
153
|
+
const qualificationRequired = process.env.BUILDCHAIN_CONSUMER_QUALIFICATION_REQUIRED === "true";
|
|
151
154
|
const admission = createPublicationAdmission({
|
|
152
155
|
registryDigest: registry.registryDigest,
|
|
153
156
|
workflowPath: required("BUILDCHAIN_AUTHORITY_WORKFLOW_PATH"),
|
|
@@ -157,6 +160,7 @@ async function main() {
|
|
|
157
160
|
runtimeSha,
|
|
158
161
|
contractDigest: controllerReceipt.runtime?.contractDigest,
|
|
159
162
|
policyDigest: gateAggregate.policyDigest,
|
|
163
|
+
gateRegistryDigest: gateBindings.registryDigest,
|
|
160
164
|
controllerReceiptDigest: controllerReceipt.digest,
|
|
161
165
|
runnerProvenanceDigest: runnerProvenance.receiptDigest,
|
|
162
166
|
controlPlaneAuditDigest: controlPlaneAudit.receiptDigest,
|
|
@@ -170,9 +174,14 @@ async function main() {
|
|
|
170
174
|
nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}`,
|
|
171
175
|
issuedAt: issuedAt.toISOString(),
|
|
172
176
|
expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
|
|
177
|
+
qualification: {
|
|
178
|
+
required: qualificationRequired,
|
|
179
|
+
predicateId: process.env.BUILDCHAIN_CONSUMER_PREDICATE_ID || "",
|
|
180
|
+
predicateDigest: process.env.BUILDCHAIN_CONSUMER_PREDICATE_DIGEST || "",
|
|
181
|
+
},
|
|
173
182
|
});
|
|
174
183
|
const bindingNames = [
|
|
175
|
-
"repository", "publisherWorkflowPath", "sourceSha", "runtimeSha", "contractDigest", "policyDigest",
|
|
184
|
+
"repository", "publisherWorkflowPath", "sourceSha", "runtimeSha", "contractDigest", "policyDigest", "gateRegistryDigest",
|
|
176
185
|
"controllerReceiptDigest", "gateAggregateDigest", "environment", "product", "target", "version", "channel",
|
|
177
186
|
"artifactDigest",
|
|
178
187
|
];
|
|
@@ -21,6 +21,7 @@ const requiredPaths = [
|
|
|
21
21
|
".github/pull_request_template.md",
|
|
22
22
|
"bin/buildchain.mjs",
|
|
23
23
|
"packages/core/homebrew.js",
|
|
24
|
+
"packages/core/artifact-verification-envelope.js",
|
|
24
25
|
"packages/core/build-facts.js",
|
|
25
26
|
"packages/core/publication-package.js",
|
|
26
27
|
"packages/core/release-line-bootstrap.js",
|
|
@@ -239,6 +240,9 @@ if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildcha
|
|
|
239
240
|
if (rootPackage.exports?.["./controller-evidence"] !== "./packages/core/controller-evidence.js") {
|
|
240
241
|
throw new Error("root package must export @kungfu-tech/buildchain/controller-evidence");
|
|
241
242
|
}
|
|
243
|
+
if (rootPackage.exports?.["./artifact-verification-envelope"] !== "./packages/core/artifact-verification-envelope.js") {
|
|
244
|
+
throw new Error("root package must export @kungfu-tech/buildchain/artifact-verification-envelope");
|
|
245
|
+
}
|
|
242
246
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
243
247
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
244
248
|
}
|
|
@@ -843,6 +847,7 @@ for (const requiredSnippet of [
|
|
|
843
847
|
"expected-publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
844
848
|
"publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
845
849
|
"name: Plan exact publication version",
|
|
850
|
+
"name: Install exact publication planning dependencies",
|
|
846
851
|
"DRY_RUN: ${{ inputs.dry-run }}",
|
|
847
852
|
"if (dryRun) {",
|
|
848
853
|
"Enforce Buildchain stable release canary gate",
|
|
@@ -444,6 +444,8 @@ function cliCommandMeta(id) {
|
|
|
444
444
|
"publication-artifact": { group: "reusable-build", purpose: "Generate publication artifact manifests, passports, and source bundles for paper/report repositories." },
|
|
445
445
|
"publication-artifact-manifest": { group: "reusable-build", purpose: "Write a site-consumable publication artifact manifest, publication passport, and source bundle." },
|
|
446
446
|
"publication-artifact-npm-package": { group: "reusable-build", purpose: "Synthesize the declared npm paper package from a publication artifact manifest, passport, registry, source bundle, and primary artifact." },
|
|
447
|
+
project: { group: "release-passport-trust", purpose: "Inspect read-only consumer projection command families." },
|
|
448
|
+
"project-kfx-admission": { group: "release-passport-trust", purpose: "Project one verified sealed envelope into direct KFX admission inputs without reconstructing evidence." },
|
|
447
449
|
"release-dry-run": { group: "governance-versioning", purpose: "Explain what a channel merge would publish before the PR is merged." },
|
|
448
450
|
"release-line-open": { group: "governance-versioning", purpose: "Plan or write the initial version-state commit for a new minor release line." },
|
|
449
451
|
"release-propagation": { group: "site-and-propagation", purpose: "Plan channel-preserving downstream release PRs and write exact upstream release locks." },
|
|
@@ -455,6 +457,7 @@ function cliCommandMeta(id) {
|
|
|
455
457
|
validate: { group: "getting-started", purpose: "Validate .buildchain/buildchain.toml and declared lifecycle surfaces." },
|
|
456
458
|
verify: { group: "release-passport-trust", purpose: "Inspect release and artifact verification command families." },
|
|
457
459
|
"verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
|
|
460
|
+
"verify-artifact-envelope": { group: "release-passport-trust", purpose: "Verify exact roots, identity, lifecycle, revocation, and an existing KFD assessment in a sealed artifact envelope." },
|
|
458
461
|
"verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
|
|
459
462
|
"verify-observability-log": { group: "observability-diagnostics", purpose: "Verify Buildchain observability log events." },
|
|
460
463
|
"verify-publication-admission": { group: "release-passport-trust", purpose: "Independently verify sealed publication admission, runner provenance, control-plane audit, nonce freshness, and exact artifact bindings." },
|
|
@@ -485,6 +488,7 @@ function nodeApiMeta(exportName) {
|
|
|
485
488
|
"./publication-control-plane-audit": { group: "release-passport-trust", summary: "Read-only publication control-plane snapshot evaluation APIs." },
|
|
486
489
|
"./buildchain-publication-authority": { group: "release-passport-trust", summary: "Buildchain-owned closed-world publication authority descriptor registry." },
|
|
487
490
|
"./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
|
|
491
|
+
"./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
|
|
488
492
|
"./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
|
|
489
493
|
"./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
|
|
490
494
|
"./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
|