@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.
Files changed (44) hide show
  1. package/actions/promote-buildchain-ref/README.md +13 -0
  2. package/actions/run-lifecycle/README.md +5 -0
  3. package/bin/buildchain.mjs +62 -0
  4. package/dist/site/buildchain-contract.json +50 -25
  5. package/dist/site/buildchain-site.json +82 -41
  6. package/dist/site/capability-registry.json +3 -3
  7. package/dist/site/cli-registry.json +24 -0
  8. package/dist/site/controller-registry.json +24 -4
  9. package/dist/site/kfd-claims.json +78 -16
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +10 -10
  12. package/dist/site/node-api-registry.json +20 -7
  13. package/dist/site/page-registry.json +68 -27
  14. package/dist/site/public-surface-audit.json +65 -15
  15. package/dist/site/publication-registry.json +4 -4
  16. package/dist/site/release-provenance.json +1 -0
  17. package/dist/site/site-manifest.json +14 -14
  18. package/dist/site/workflow-registry.json +25 -9
  19. package/docs/MAP.md +1 -0
  20. package/docs/artifact-verification-envelope.md +113 -0
  21. package/docs/cli.md +22 -0
  22. package/docs/install.md +1 -3
  23. package/docs/publication-authority.md +26 -0
  24. package/docs/publish-transaction.md +1 -1
  25. package/docs/release-candidate.md +24 -0
  26. package/docs/release-governance.md +12 -6
  27. package/docs/release-propagation.md +3 -0
  28. package/docs/reusable-build-surface.md +13 -0
  29. package/docs/versioning.md +1 -0
  30. package/package.json +2 -1
  31. package/packages/core/README.md +13 -0
  32. package/packages/core/artifact-passport.js +6 -1
  33. package/packages/core/artifact-verification-envelope.js +386 -0
  34. package/packages/core/buildchain-config.js +6 -2
  35. package/packages/core/controller-evidence.js +2 -3
  36. package/packages/core/index.js +16 -0
  37. package/packages/core/publication-authority.js +281 -12
  38. package/packages/core/release-line-bootstrap.js +24 -0
  39. package/scripts/assemble-publication-artifact-admission.mjs +10 -0
  40. package/scripts/assemble-self-publication-admission.mjs +10 -1
  41. package/scripts/check-inventory.mjs +5 -0
  42. package/scripts/generate-site-bundle.mjs +4 -0
  43. package/scripts/release-candidate-resolver.mjs +52 -29
  44. package/scripts/run-lifecycle-core.mjs +50 -6
@@ -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 validateGateAggregate(gateAggregate, { admission, passport }) {
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
- if (![admission.sourceSha, passportSourceSha].includes(gateAggregate.sourceSha)) {
396
- throw new Error("gate aggregate source SHA mismatch");
397
- }
398
- normalizeDigest(gateAggregate.registry?.digest, "gateAggregate.registry.digest");
413
+ const registryDigest = normalizeDigest(
414
+ gateAggregate.registry?.digest,
415
+ "gateAggregate.registry.digest",
416
+ );
399
417
  const policyDigest = normalizeDigest(gateAggregate.matrixDigest, "gateAggregate.matrixDigest");
400
- return { gateAggregateDigest: actualDigest, policyDigest };
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 { gateAggregateDigest: actualDigest, policyDigest };
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." },