@kungfu-tech/buildchain 4.0.1-alpha.4 → 4.0.1-alpha.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.
Files changed (87) hide show
  1. package/architecture/decisions/0002-next-development-transition.md +101 -0
  2. package/architecture/internal-capabilities.json +52 -3
  3. package/architecture/maintainability-policy.json +22 -10
  4. package/architecture/v3-core-mechanism-inventory.json +89 -0
  5. package/architecture/v4-capability-state-machine-manifest.json +73 -0
  6. package/architecture/v4-compatibility-facts-parity.json +77 -0
  7. package/architecture/v4-next-development-parity.json +192 -0
  8. package/architecture/v4-release-train-parity.json +1 -0
  9. package/architecture/v4-tail-reseal-parity.json +150 -0
  10. package/bin/buildchain.mjs +16 -1
  11. package/bin/internal/command-registry.mjs +2 -0
  12. package/bin/internal/compatibility-facts-cli.mjs +101 -0
  13. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  14. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  15. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  16. package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
  17. package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
  18. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
  19. package/contracts/next-development-request-v1.schema.json +66 -0
  20. package/contracts/next-development-transition-v1.schema.json +292 -0
  21. package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
  22. package/contracts/v4-tail-reseal-v1.schema.json +276 -0
  23. package/dist/site/agent-index.json +1 -0
  24. package/dist/site/artifact-schemas.json +2 -0
  25. package/dist/site/buildchain-contract.json +1388 -23
  26. package/dist/site/buildchain-site.json +257 -19
  27. package/dist/site/capability-registry.json +10 -8
  28. package/dist/site/cli-registry.json +106 -0
  29. package/dist/site/kfd-claims.json +187 -12
  30. package/dist/site/kfd-upstream-aggregate.json +1 -1
  31. package/dist/site/manual-registry.json +35 -4
  32. package/dist/site/node-api-registry.json +6579 -4493
  33. package/dist/site/page-registry.json +233 -11
  34. package/dist/site/public-surface-audit.json +241 -7
  35. package/dist/site/publication-authority-registry.json +6 -1
  36. package/dist/site/publication-registry.json +4 -4
  37. package/dist/site/release-provenance.json +7 -0
  38. package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
  39. package/dist/site/site-manifest.json +24 -8
  40. package/dist/site/workflow-registry.json +36 -4
  41. package/docs/MAP.md +4 -1
  42. package/docs/cli-reference.md +125 -0
  43. package/docs/dev-alpha-candidate-patrol.md +8 -0
  44. package/docs/next-development-transition.md +118 -0
  45. package/docs/node-api-reference.md +250 -117
  46. package/docs/v4-compatibility-facts.md +91 -0
  47. package/docs/v4-tail-reseal.md +73 -0
  48. package/docs/versioning.md +4 -3
  49. package/package.json +14 -5
  50. package/packages/core/buildchain-agent-manuals.js +2 -0
  51. package/packages/core/buildchain-compatibility-authority.js +336 -0
  52. package/packages/core/buildchain-compatibility-fact.js +394 -0
  53. package/packages/core/buildchain-compatibility-facts.json +217 -0
  54. package/packages/core/buildchain-compatibility-proof.js +566 -0
  55. package/packages/core/buildchain-contract.js +93 -141
  56. package/packages/core/channel-candidate.js +8 -0
  57. package/packages/core/channel-promotion-baseline.js +55 -0
  58. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  59. package/packages/core/dev-delivery-execution-transfer.js +21 -1
  60. package/packages/core/dev-delivery-process-boundary.js +19 -6
  61. package/packages/core/dev-delivery-provider-heartbeat.js +18 -6
  62. package/packages/core/kungfu-temporal-fact.js +557 -0
  63. package/packages/core/next-development-candidate-reservation.js +186 -0
  64. package/packages/core/next-development-controller.js +728 -0
  65. package/packages/core/next-development-projection.js +287 -0
  66. package/packages/core/next-development-transition.js +738 -0
  67. package/packages/core/paper-agent-entry.js +11 -5
  68. package/packages/core/paper.js +16 -5
  69. package/packages/core/v4-canonical-contracts.js +5 -0
  70. package/packages/core/v4-floating-consumer-policy.js +29 -9
  71. package/packages/core/v4-tail-reseal-contract.js +26 -0
  72. package/packages/core/v4-tail-reseal-github.js +178 -0
  73. package/packages/core/v4-tail-reseal-receipt.js +225 -0
  74. package/packages/core/v4-tail-reseal.js +594 -0
  75. package/scripts/buildchain-cli-help.mjs +20 -0
  76. package/scripts/check-inventory.mjs +11 -0
  77. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  78. package/scripts/generate-next-development-guidance.mjs +49 -0
  79. package/scripts/generate-site-bundle.mjs +16 -3
  80. package/scripts/init-repo.mjs +22 -4
  81. package/scripts/next-development-self-dogfood-harness.mjs +460 -0
  82. package/scripts/next-development-self-dogfood.mjs +597 -0
  83. package/scripts/next-development-transition.mjs +47 -0
  84. package/scripts/site-capability-metadata.mjs +12 -0
  85. package/scripts/v4-consumer-policy.mjs +1 -5
  86. package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
  87. package/scripts/v4-tail-reseal.mjs +416 -0
@@ -7,6 +7,15 @@ import {
7
7
  normalizeBuildchainRef,
8
8
  parseBuildchainRefIdentity,
9
9
  } from "./buildchain-channel-identity.js";
10
+ import {
11
+ createHistoricalBuildchainCompatibilityFacts,
12
+ createHistoricalBuildchainCompatibilityProofs,
13
+ } from "./buildchain-compatibility-fact.js";
14
+ import {
15
+ assertBuildchainCompatibilityProjection,
16
+ createBuildchainCompatibilityProofRegistry,
17
+ } from "./buildchain-compatibility-authority.js";
18
+ import { evaluateBuildchainContractLock as evaluateFactBackedBuildchainContractLock } from "./buildchain-compatibility-proof.js";
10
19
  import { createControllerRegistry } from "./controller-evidence.js";
11
20
  import { enumerateWorkflowInputs } from "./public-surface-audit.js";
12
21
  import { devDeliveryWorkflowContractSurface } from "./dev-delivery-contract-surface.js";
@@ -18,11 +27,47 @@ export {
18
27
  parseBuildchainRefIdentity,
19
28
  };
20
29
 
30
+ export {
31
+ BUILDCHAIN_COMPATIBILITY_FACT_REGISTRY_SCHEMA,
32
+ BUILDCHAIN_COMPATIBILITY_FACT_SCHEMA,
33
+ BUILDCHAIN_COMPATIBILITY_PROOF_REGISTRY_SCHEMA,
34
+ BUILDCHAIN_COMPATIBILITY_PROOF_SCHEMA,
35
+ BUILDCHAIN_COMPATIBILITY_RELEASE_EVIDENCE_SCHEMA,
36
+ createBuildchainCompatibilityFact,
37
+ createBuildchainCompatibilityFactRegistry,
38
+ createBuildchainCompatibilityProof,
39
+ createHistoricalBuildchainCompatibilityFacts,
40
+ createHistoricalBuildchainCompatibilityProofs,
41
+ verifyBuildchainCompatibilityFact,
42
+ verifyBuildchainCompatibilityFactRegistry,
43
+ verifyBuildchainCompatibilityProof,
44
+ } from "./buildchain-compatibility-fact.js";
45
+ export {
46
+ assertBuildchainCompatibilityProjection,
47
+ createBuildchainCompatibilityPathQuery,
48
+ createBuildchainCompatibilityProofRegistry,
49
+ createBuildchainCompatibilityReleaseEvidence,
50
+ resolveBuildchainCompatibilityProof,
51
+ verifyBuildchainCompatibilityPath,
52
+ } from "./buildchain-compatibility-authority.js";
53
+ export {
54
+ BUILDCHAIN_COMPATIBILITY_VERIFICATION_RECEIPT_SCHEMA,
55
+ BUILDCHAIN_LEGACY_COMPATIBILITY_VERIFICATION_RECEIPT_SCHEMA,
56
+ createBuildchainCompatibilityVerificationReceipt,
57
+ verifyBuildchainCompatibilityVerificationReceipt,
58
+ } from "./buildchain-compatibility-proof.js";
59
+
21
60
  export const BUILDCHAIN_RUNTIME_CONTRACT_WORLD = "kungfu-buildchain-runtime-contract-world";
22
61
  export const BUILDCHAIN_CONTRACT_LOCK = "kungfu-buildchain-contract-lock";
23
62
 
24
63
  const DEFAULT_POLICY = "major-compatible";
25
- const FLOATING_CLASSES = new Set(["stable", "alpha"]);
64
+
65
+ function compatibilityAuthority() {
66
+ return {
67
+ compatibilityFacts: createHistoricalBuildchainCompatibilityFacts(),
68
+ compatibilityProofs: createHistoricalBuildchainCompatibilityProofs(),
69
+ };
70
+ }
26
71
 
27
72
  function optionalString(value) {
28
73
  return value === undefined || value === null ? "" : String(value);
@@ -796,26 +841,60 @@ export function createBuildchainContractWorld({
796
841
  version: pkg.version || "",
797
842
  repository: pkg.repository?.url || pkg.repository || "https://github.com/kungfu-systems/buildchain",
798
843
  },
799
- majorLine,
800
- compatibilityPolicy: DEFAULT_POLICY,
844
+ majorLine, compatibilityPolicy: DEFAULT_POLICY,
845
+ ...compatibilityAuthority(),
801
846
  surfaces,
802
847
  };
803
848
  return finalizeBuildchainContractWorld(base);
804
849
  }
805
850
 
806
851
  export function finalizeBuildchainContractWorld(contractWorld) {
852
+ if (!Array.isArray(contractWorld?.compatibilityFacts) || !Array.isArray(contractWorld?.compatibilityProofs)) {
853
+ throw new Error("contract world requires compatibility Facts and their proof projection");
854
+ }
855
+ const sourceSurfaces = (contractWorld.surfaces || []).map((entry) => ({ ...entry }));
856
+ const proofRegistry = createBuildchainCompatibilityProofRegistry({
857
+ proofs: contractWorld.compatibilityProofs,
858
+ surfaces: sourceSurfaces,
859
+ majorLine: contractWorld.majorLine,
860
+ });
861
+ if (JSON.stringify(contractWorld.compatibilityFacts) !== JSON.stringify(proofRegistry.facts)) {
862
+ throw new Error("contract world compatibility Facts drift from the verified registry");
863
+ }
807
864
  const world = {
808
865
  ...contractWorld,
809
- surfaces: (contractWorld.surfaces || []).map((entry) => ({ ...entry })),
866
+ compatibilityFacts: proofRegistry.facts,
867
+ compatibilityFactRegistryRoot: proofRegistry.factRegistryRoot,
868
+ compatibilityFactCutRoot: proofRegistry.factCutRoot,
869
+ compatibilityProofs: proofRegistry.proofs,
870
+ compatibilityProofRegistryRoot: proofRegistry.registryRoot,
871
+ surfaces: sourceSurfaces.map((entry) => {
872
+ const surfaceEntry = { ...entry };
873
+ delete surfaceEntry.compatibleBreakingDigests;
874
+ delete surfaceEntry.compatibilityProofRoots;
875
+ delete surfaceEntry.compatibilityFactRoots;
876
+ const projection = proofRegistry.projections[entry.id] || [];
877
+ if (projection.length > 0) {
878
+ surfaceEntry.compatibleBreakingDigests = projection.map((item) => item.breakingDigest);
879
+ surfaceEntry.compatibilityProofRoots = projection.map((item) => item.proofRoot);
880
+ surfaceEntry.compatibilityFactRoots = projection.map((item) => item.factRoot);
881
+ }
882
+ return surfaceEntry;
883
+ }),
810
884
  };
811
885
  const compatibilityModel = {
812
886
  schemaVersion: world.schemaVersion,
813
887
  contract: world.contract,
814
888
  majorLine: world.majorLine,
889
+ compatibilityFactRegistryRoot: world.compatibilityFactRegistryRoot,
890
+ compatibilityFactCutRoot: world.compatibilityFactCutRoot,
891
+ compatibilityProofRegistryRoot: world.compatibilityProofRegistryRoot,
815
892
  surfaces: world.surfaces.map((entry) => ({
816
893
  id: entry.id,
817
894
  kind: entry.kind,
818
895
  breakingDigest: entry.breakingDigest,
896
+ compatibilityProofRoots: entry.compatibilityProofRoots || [],
897
+ compatibilityFactRoots: entry.compatibilityFactRoots || [],
819
898
  })),
820
899
  };
821
900
  const digestModel = {
@@ -847,6 +926,9 @@ export function createBuildchainContractLock({
847
926
  contract: contractWorld.contract,
848
927
  contractDigest: contractWorld.contractDigest,
849
928
  compatibilityDigest: contractWorld.compatibilityDigest,
929
+ compatibilityFactRegistryRoot: contractWorld.compatibilityFactRegistryRoot,
930
+ compatibilityFactCutRoot: contractWorld.compatibilityFactCutRoot,
931
+ compatibilityProofRegistryRoot: contractWorld.compatibilityProofRegistryRoot,
850
932
  majorLine: contractWorld.majorLine,
851
933
  compatibilityPolicy,
852
934
  acceptedAt,
@@ -854,6 +936,8 @@ export function createBuildchainContractLock({
854
936
  id: entry.id,
855
937
  kind: entry.kind,
856
938
  breakingDigest: entry.breakingDigest,
939
+ compatibilityProofRoots: entry.compatibilityProofRoots || [],
940
+ compatibilityFactRoots: entry.compatibilityFactRoots || [],
857
941
  })),
858
942
  },
859
943
  };
@@ -890,143 +974,8 @@ export function readBuildchainContractLock(filePath) {
890
974
  return value;
891
975
  }
892
976
 
893
- function surfaceMap(surfaces = []) {
894
- return new Map(surfaces.map((entry) => [entry.id, entry]));
895
- }
896
-
897
- function evaluateChannelBinding({
898
- lock,
899
- workflowShellRef,
900
- runtimeRef,
901
- expectedChannel,
902
- expectedMajor,
903
- allowOpaqueRuntime,
904
- }) {
905
- if (!workflowShellRef && !expectedChannel && !expectedMajor) return null;
906
- return evaluateBuildchainChannelBinding({
907
- workflowShellRef,
908
- runtimeRef,
909
- lockRef: lock?.buildchain?.ref || "",
910
- lockMajorLine: lock?.buildchain?.majorLine || "",
911
- expectedChannel,
912
- expectedMajor,
913
- allowOpaqueRuntime,
914
- });
915
- }
916
-
917
- export function evaluateBuildchainContractLock({
918
- lock,
919
- current,
920
- runtimeRef = "",
921
- runtimeSha = "",
922
- runtimeClass = "",
923
- compatibilityPolicy = "",
924
- workflowShellRef = "",
925
- expectedChannel = "",
926
- expectedMajor = "",
927
- allowOpaqueRuntime = false,
928
- } = {}) {
929
- if (!current || current.contract !== BUILDCHAIN_RUNTIME_CONTRACT_WORLD) {
930
- throw new Error("current must be a Buildchain runtime contract world");
931
- }
932
- const binding = evaluateChannelBinding({
933
- lock,
934
- workflowShellRef,
935
- runtimeRef,
936
- expectedChannel,
937
- expectedMajor,
938
- allowOpaqueRuntime,
939
- });
940
- if (binding?.ok === false) {
941
- return {
942
- ok: false,
943
- status: binding.status,
944
- drift: false,
945
- compatible: false,
946
- issueRecommended: false,
947
- reasons: binding.reasons,
948
- channelBinding: binding,
949
- };
950
- }
951
- const floatingRuntime = FLOATING_CLASSES.has(runtimeClass);
952
- if (!floatingRuntime) {
953
- return {
954
- ok: true,
955
- status: "non-floating-runtime",
956
- drift: false,
957
- compatible: true,
958
- issueRecommended: false,
959
- reason: `runtime class ${runtimeClass || "unknown"} is not a stable floating ref`,
960
- };
961
- }
962
- if (!lock) {
963
- return {
964
- ok: true,
965
- status: "missing-lock",
966
- drift: false,
967
- compatible: true,
968
- issueRecommended: false,
969
- reason: "consumer repository has no Buildchain contract lock",
970
- };
971
- }
972
- const accepted = lock.buildchain || {};
973
- const policy = compatibilityPolicy || accepted.compatibilityPolicy || DEFAULT_POLICY;
974
- const shaDrift = !!accepted.resolvedSha && !!runtimeSha && accepted.resolvedSha !== runtimeSha;
975
- const contractDrift = !!accepted.contractDigest && accepted.contractDigest !== current.contractDigest;
976
- if (!shaDrift && !contractDrift) {
977
- return {
978
- ok: true,
979
- status: "unchanged",
980
- drift: false,
981
- compatible: true,
982
- issueRecommended: false,
983
- policy,
984
- accepted,
985
- current: contractSummary(current, runtimeRef, runtimeSha),
986
- };
987
- }
988
- const reasons = [];
989
- if (accepted.contract !== current.contract) {
990
- reasons.push(`contract changed from ${accepted.contract || "(unknown)"} to ${current.contract}`);
991
- }
992
- if (accepted.majorLine && accepted.majorLine !== current.majorLine) {
993
- reasons.push(`major line changed from ${accepted.majorLine} to ${current.majorLine}`);
994
- }
995
- if (policy === "exact" && accepted.contractDigest !== current.contractDigest) {
996
- reasons.push("exact policy requires the contract digest to remain unchanged");
997
- }
998
- if (!["major-compatible", "allow-additive", "exact"].includes(policy)) {
999
- reasons.push(`unsupported compatibility policy: ${policy}`);
1000
- }
1001
- const currentSurfaces = surfaceMap(current.surfaces);
1002
- for (const oldSurface of accepted.surfaces || []) {
1003
- const nextSurface = currentSurfaces.get(oldSurface.id);
1004
- if (!nextSurface) {
1005
- reasons.push(`surface removed: ${oldSurface.id}`);
1006
- continue;
1007
- }
1008
- const compatibleBreakingDigests = new Set(nextSurface.compatibleBreakingDigests || []);
1009
- if (
1010
- nextSurface.breakingDigest !== oldSurface.breakingDigest
1011
- && !compatibleBreakingDigests.has(oldSurface.breakingDigest)
1012
- ) {
1013
- reasons.push(`surface breaking digest changed: ${oldSurface.id}`);
1014
- }
1015
- }
1016
- const compatible = reasons.length === 0;
1017
- return {
1018
- ok: compatible,
1019
- status: compatible ? "compatible-drift" : "breaking-drift",
1020
- drift: shaDrift || contractDrift,
1021
- shaDrift,
1022
- contractDrift,
1023
- compatible,
1024
- issueRecommended: true,
1025
- policy,
1026
- reasons,
1027
- accepted,
1028
- current: contractSummary(current, runtimeRef, runtimeSha),
1029
- };
977
+ export function evaluateBuildchainContractLock(options = {}) {
978
+ return evaluateFactBackedBuildchainContractLock(options);
1030
979
  }
1031
980
 
1032
981
  export function contractSummary(contractWorld, runtimeRef = "", runtimeSha = "") {
@@ -1036,6 +985,9 @@ export function contractSummary(contractWorld, runtimeRef = "", runtimeSha = "")
1036
985
  contract: contractWorld.contract,
1037
986
  contractDigest: contractWorld.contractDigest,
1038
987
  compatibilityDigest: contractWorld.compatibilityDigest,
988
+ compatibilityFactRegistryRoot: contractWorld.compatibilityFactRegistryRoot,
989
+ compatibilityFactCutRoot: contractWorld.compatibilityFactCutRoot,
990
+ compatibilityProofRegistryRoot: contractWorld.compatibilityProofRegistryRoot,
1039
991
  majorLine: contractWorld.majorLine,
1040
992
  surfaceCount: Array.isArray(contractWorld.surfaces) ? contractWorld.surfaces.length : 0,
1041
993
  };
@@ -97,6 +97,14 @@ export function decideChannelCandidate(input) {
97
97
  const sourceSha = sha(input.sourceSha, "sourceSha");
98
98
  const targetSha = sha(input.targetSha, "targetSha");
99
99
  const selection = normalizeChannelCandidateSelection(input.selection);
100
+ if (
101
+ selection?.versionReservation &&
102
+ selection.versionReservation.candidateSha !== sourceSha
103
+ ) {
104
+ throw new Error(
105
+ "selection version reservation must bind the selected source SHA",
106
+ );
107
+ }
100
108
  const now = required(input.now || new Date().toISOString(), "now");
101
109
  const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
102
110
  if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
@@ -1,5 +1,10 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
 
3
+ import {
4
+ nextDevelopmentPatrolContext,
5
+ normalizeNextDevelopmentVersionReservation,
6
+ } from "./next-development-candidate-reservation.js";
7
+
3
8
  const SHA = /^[0-9a-f]{40}$/;
4
9
 
5
10
  function text(value = "") {
@@ -59,6 +64,18 @@ export function normalizeChannelCandidateSelection(input) {
59
64
  ),
60
65
  }
61
66
  : {}),
67
+ ...(input.versionReservation
68
+ ? {
69
+ versionReservation: normalizeNextDevelopmentVersionReservation(
70
+ input.versionReservation,
71
+ {
72
+ reservationSha: input.versionReservation.reservationSha,
73
+ candidateSha: input.versionReservation.candidateSha,
74
+ targetVersion: input.versionReservation.targetVersion,
75
+ },
76
+ ),
77
+ }
78
+ : {}),
62
79
  };
63
80
  }
64
81
 
@@ -117,6 +134,7 @@ export async function resolvePatrolSourceInputs({
117
134
  let comparison = headComparison;
118
135
  let workflowEvidence = [];
119
136
  let skippedNewerCommitCount = 0;
137
+ let versionReservation;
120
138
  let qualificationError;
121
139
  if (
122
140
  headComparison.status === "ahead" &&
@@ -130,6 +148,8 @@ export async function resolvePatrolSourceInputs({
130
148
  ),
131
149
  ]);
132
150
  try {
151
+ const { preparations, ignoredSourceShas } =
152
+ nextDevelopmentPatrolContext(sourceHistory);
133
153
  const selected = selectQualifiedSource({
134
154
  sourceHistory,
135
155
  workflowRunsByPath: new Map(
@@ -141,8 +161,42 @@ export async function resolvePatrolSourceInputs({
141
161
  requiredWorkflowPaths,
142
162
  now: options.now,
143
163
  maxAgeSeconds: options.maxAgeSeconds,
164
+ ignoredSourceShas,
144
165
  });
145
166
  ({ sourceSha, skippedNewerCommitCount, workflowEvidence } = selected);
167
+ const selectedIndex = sourceHistory.findIndex(
168
+ (entry) =>
169
+ (typeof entry === "string" ? entry : entry?.sha) === sourceSha,
170
+ );
171
+ const reservation = preparations.find(
172
+ ({ index }) => index > selectedIndex,
173
+ )?.preparation;
174
+ if (reservation) {
175
+ if (
176
+ typeof client.readNextDevelopmentVersionReservation !== "function"
177
+ ) {
178
+ throw new Error(
179
+ "candidate follows next-development preparation but reservation readback is unavailable",
180
+ );
181
+ }
182
+ versionReservation = normalizeNextDevelopmentVersionReservation(
183
+ await client.readNextDevelopmentVersionReservation({
184
+ reservationSha: reservation.sha,
185
+ candidateSha: sourceSha,
186
+ targetVersion: reservation.targetVersion,
187
+ }),
188
+ {
189
+ reservationSha: reservation.sha,
190
+ candidateSha: sourceSha,
191
+ targetVersion: reservation.targetVersion,
192
+ },
193
+ );
194
+ if (versionReservation.status !== "current") {
195
+ throw new Error(
196
+ `next-development version reservation is ${versionReservation.status} for candidate ${sourceSha}`,
197
+ );
198
+ }
199
+ }
146
200
  if (sourceSha !== observedSourceHeadSha)
147
201
  comparison = await client.compare(boundarySha, sourceSha);
148
202
  } catch (error) {
@@ -158,6 +212,7 @@ export async function resolvePatrolSourceInputs({
158
212
  comparison,
159
213
  workflowEvidence,
160
214
  skippedNewerCommitCount,
215
+ versionReservation,
161
216
  qualificationError,
162
217
  };
163
218
  }
@@ -82,7 +82,9 @@ export function selectLatestQualifiedSource({
82
82
  requiredWorkflowPaths,
83
83
  now,
84
84
  maxAgeSeconds,
85
+ ignoredSourceShas = [],
85
86
  }) {
87
+ const ignored = new Set(ignoredSourceShas);
86
88
  const latestByPath = new Map(
87
89
  requiredWorkflowPaths.map((workflow) => [
88
90
  workflow,
@@ -90,7 +92,11 @@ export function selectLatestQualifiedSource({
90
92
  ]),
91
93
  );
92
94
  for (let index = 0; index < sourceHistory.length; index += 1) {
93
- const sourceSha = sourceHistory[index];
95
+ const sourceSha =
96
+ typeof sourceHistory[index] === "string"
97
+ ? sourceHistory[index]
98
+ : sourceHistory[index]?.sha;
99
+ if (ignored.has(sourceSha)) continue;
94
100
  const rows = requiredWorkflowPaths.map((workflow) =>
95
101
  latestByPath.get(workflow).get(sourceSha),
96
102
  );
@@ -115,7 +121,9 @@ export function selectLatestQualifiedSource({
115
121
  };
116
122
  }
117
123
  }
118
- const staleSuccessfulPair = sourceHistory.some((sourceSha) => {
124
+ const staleSuccessfulPair = sourceHistory.some((entry) => {
125
+ const sourceSha = typeof entry === "string" ? entry : entry?.sha;
126
+ if (ignored.has(sourceSha)) return false;
119
127
  const rows = requiredWorkflowPaths.map((workflow) =>
120
128
  latestByPath.get(workflow).get(sourceSha),
121
129
  );
@@ -13,6 +13,7 @@ import {
13
13
  devDeliveryText as text,
14
14
  devDeliveryTimestamp as timestamp,
15
15
  } from "./dev-delivery-common.js";
16
+ import { verifyNativeProofReuseDecision } from "./dev-delivery-native-proof.js";
16
17
  import { verifyNativeQualificationProof } from "./dev-delivery-native-proof.js";
17
18
  import { verifyNativeExecutionFailureOutcome } from "./dev-delivery-execution-failure.js";
18
19
 
@@ -408,8 +409,28 @@ function verifySuccessOutcome(transfer, directory, warrant) {
408
409
  "native proof reuse decision root",
409
410
  );
410
411
  delete decisionBody.decisionRoot;
412
+ const reuseVerification = verifyNativeProofReuseDecision(decision, {
413
+ proof,
414
+ current: {
415
+ sourceHead: warrant.sourceHead,
416
+ sourceIdentityRoot: warrant.sourceIdentityRoot,
417
+ sourcePatchRoot: warrant.sourcePatchRoot,
418
+ planRoot: warrant.planRoot,
419
+ closureRoot: warrant.closureRoot,
420
+ dependencyRoot: warrant.dependencyRoot,
421
+ toolchainRoot: warrant.toolchainRoot,
422
+ environmentRoot: warrant.environmentRoot,
423
+ nativeCommandRoot: warrant.nativeCommandContract?.commandRoot,
424
+ currentBase: decision.currentBase,
425
+ graphKnown: decision.graphKnown,
426
+ attributionComplete: decision.attributionComplete,
427
+ changedPaths: decision.changedPaths,
428
+ renames: decision.renames,
429
+ },
430
+ });
411
431
  if (
412
432
  !proofVerification.ok ||
433
+ !reuseVerification.ok ||
413
434
  result.nativeProofRoot !== transfer.nativeProofRoot ||
414
435
  proof.proofRoot !== transfer.nativeProofRoot ||
415
436
  result.nativeReuseDecisionRoot !== transfer.nativeReuseDecisionRoot ||
@@ -420,7 +441,6 @@ function verifySuccessOutcome(transfer, directory, warrant) {
420
441
  proof.repository !== transfer.warrant.repository ||
421
442
  proof.protectedBase !== transfer.warrant.protectedBase ||
422
443
  proof.sourceHead !== transfer.warrant.sourceHead ||
423
- proof.qualifiedBase !== warrant.qualifiedBase ||
424
444
  proof.sourceIdentityRoot !== warrant.sourceIdentityRoot ||
425
445
  proof.sourcePatchRoot !== warrant.sourcePatchRoot ||
426
446
  proof.planRoot !== warrant.planRoot ||
@@ -30,7 +30,7 @@ const PROVIDER_CREDENTIAL_NAME =
30
30
  /(?:^(?:ACTIONS_ID_TOKEN_REQUEST_TOKEN|ACTIONS_RUNTIME_TOKEN|AWS_SESSION_TOKEN|AWS_WEB_IDENTITY_TOKEN_FILE|AZURE_FEDERATED_TOKEN_FILE|GH_TOKEN|GITHUB_TOKEN|GOOGLE_GHA_CREDS_PATH|KUNGFU_GITHUB_TOKEN|NPM_TOKEN|SSH_AUTH_SOCK)$|(?:^|_)(?:(?:api|access|auth|publish|release|provider|promotion|governance|approval|mutation|write)_?(?:key|token)|authorization|credentials?|password|secret|private_key|client_secret|signing_key)(?:_|$))/iu;
31
31
  const HOSTED_RUNNER_BOUNDARY = "github-actions-runner-worker/v1";
32
32
  const GITHUB_HOSTED_LINUX_WORKER =
33
- /^\/home\/runner\/runners\/(\d+\.\d+\.\d+)\/bin\/Runner\.Worker$/u;
33
+ /^\/home\/runner\/(?:runners|actions-runner\/cached)\/(\d+\.\d+\.\d+)\/bin\/Runner\.Worker$/u;
34
34
  function text(value, label) {
35
35
  const normalized = normalizedText(value);
36
36
  if (!normalized) throw new Error(`${label} is required`);
@@ -61,10 +61,16 @@ function processCredentialNames(procRoot, pid, readFileSync) {
61
61
  .split("\0")
62
62
  .filter((entry) => {
63
63
  const separator = entry.indexOf("=");
64
+ const name = entry.slice(0, separator);
65
+ const value = entry.slice(separator + 1);
64
66
  return (
65
67
  separator > 0 &&
66
- Boolean(entry.slice(separator + 1)) &&
67
- isCredentialVariableName(entry.slice(0, separator))
68
+ Boolean(value) &&
69
+ !(
70
+ name === "BUILDCHAIN_CREDENTIAL_ANCESTRY_BOUNDARY" &&
71
+ value === HOSTED_RUNNER_BOUNDARY
72
+ ) &&
73
+ isCredentialVariableName(name)
68
74
  );
69
75
  })
70
76
  .map((entry) => entry.slice(0, entry.indexOf("=")));
@@ -108,9 +114,11 @@ function hostedBoundary(procRoot, pid, command, readlinkSync) {
108
114
  const executable = ancestryRead(readlinkSync, link);
109
115
  if (
110
116
  !GITHUB_HOSTED_LINUX_WORKER.test(executable) ||
111
- !new Set([executable, path.posix.basename(executable)]).has(command[0])
117
+ path.posix.basename(command[0]) !== path.posix.basename(executable)
112
118
  ) {
113
- throw new Error("credential ancestry found an untrusted Runner.Worker");
119
+ throw new Error(
120
+ `credential ancestry found an untrusted Runner.Worker: executable=${JSON.stringify(executable)} argv0=${JSON.stringify(command[0])}`,
121
+ );
114
122
  }
115
123
  return { pid, kind: HOSTED_RUNNER_BOUNDARY, executable };
116
124
  }
@@ -163,7 +171,11 @@ export function assertCredentiallessProcessAncestry(options = {}) {
163
171
  return inspection;
164
172
  }
165
173
 
166
- export function devDeliveryProviderJobByName(jobs, expectedName) {
174
+ export function devDeliveryProviderJobByName(
175
+ jobs,
176
+ expectedName,
177
+ { allowMissing = false } = {},
178
+ ) {
167
179
  const exactName = text(expectedName, "provider job name");
168
180
  const suffix = ` / ${exactName}`;
169
181
  const matches = jobs.filter((job) => {
@@ -173,6 +185,7 @@ export function devDeliveryProviderJobByName(jobs, expectedName) {
173
185
  (actualName.endsWith(suffix) && actualName.length > suffix.length)
174
186
  );
175
187
  });
188
+ if (matches.length === 0 && allowMissing) return null;
176
189
  if (matches.length !== 1) {
177
190
  throw new Error(`expected exactly one provider job named ${exactName}`);
178
191
  }
@@ -13,7 +13,7 @@ export const DEV_DELIVERY_PROVIDER_HEARTBEAT_SCHEMA =
13
13
  "kungfu.buildchain.provider-heartbeat-receipt/v1";
14
14
 
15
15
  function selectedWarrant(result = {}) {
16
- return result.observation?.activeWarrant || result.warrant || null;
16
+ return result.warrant || result.observation?.activeWarrant || null;
17
17
  }
18
18
 
19
19
  function exactWarrant(input = {}) {
@@ -40,8 +40,9 @@ function exactWarrant(input = {}) {
40
40
  };
41
41
  }
42
42
 
43
- function job(entries, name) {
44
- const entry = devDeliveryProviderJobByName(entries, name);
43
+ function job(entries, name, options) {
44
+ const entry = devDeliveryProviderJobByName(entries, name, options);
45
+ if (!entry) return null;
45
46
  const providerJob = {
46
47
  id: positiveInteger(entry.id, `${name} job id`),
47
48
  name,
@@ -66,13 +67,16 @@ function job(entries, name) {
66
67
  return providerJob;
67
68
  }
68
69
 
69
- function providerJobs(input, expected) {
70
+ function providerJobs(input, expected, { allowMissingSeal = false } = {}) {
70
71
  const entries = Array.isArray(input) ? input : input?.jobs;
71
72
  if (!Array.isArray(entries)) {
72
73
  throw new Error("provider heartbeat jobs readback must contain jobs");
73
74
  }
74
75
  const native = job(entries, expected.nativeJobName);
75
- const seal = job(entries, expected.sealJobName);
76
+ const seal = job(entries, expected.sealJobName, {
77
+ allowMissing: allowMissingSeal,
78
+ });
79
+ if (!seal) return null;
76
80
  if (native.id === seal.id) {
77
81
  throw new Error("provider heartbeat native and seal jobs must be distinct");
78
82
  }
@@ -206,7 +210,15 @@ export async function runDevDeliveryProviderHeartbeat(
206
210
  const entry = heartbeatEntry(result, previousStateRoot);
207
211
  heartbeats.push(entry);
208
212
  previousStateRoot = entry.nextStateRoot;
209
- jobs = providerJobs(await readJobs(), { nativeJobName, sealJobName });
213
+ jobs = providerJobs(
214
+ await readJobs(),
215
+ { nativeJobName, sealJobName },
216
+ { allowMissingSeal: true },
217
+ );
218
+ if (!jobs) {
219
+ await wait(cadence * 1000);
220
+ continue;
221
+ }
210
222
  if (terminal(jobs.native) && terminal(jobs.seal)) break;
211
223
  await wait(cadence * 1000);
212
224
  }