@kungfu-tech/buildchain 4.0.1-alpha.4 → 4.0.1-alpha.40

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 (172) hide show
  1. package/actions/release-tail/README.md +8 -0
  2. package/architecture/ci-lane-change-budget.json +187 -0
  3. package/architecture/decisions/0002-next-development-transition.md +101 -0
  4. package/architecture/internal-capabilities.json +117 -8
  5. package/architecture/maintainability-policy.json +43 -25
  6. package/architecture/release-tail-contract-inventory.json +5 -2
  7. package/architecture/v3-core-mechanism-inventory.json +89 -0
  8. package/architecture/v4-adopter-delivery-parity.json +32 -5
  9. package/architecture/v4-capability-state-machine-manifest.json +73 -0
  10. package/architecture/v4-compatibility-facts-parity.json +77 -0
  11. package/architecture/v4-floating-consumer-policy.json +6 -0
  12. package/architecture/v4-next-development-parity.json +192 -0
  13. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  14. package/architecture/v4-release-train-parity.json +1 -0
  15. package/architecture/v4-tail-reseal-parity.json +150 -0
  16. package/bin/buildchain.mjs +23 -2
  17. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  18. package/bin/internal/command-registry.mjs +3 -0
  19. package/bin/internal/compatibility-facts-cli.mjs +101 -0
  20. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  21. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  22. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  23. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  24. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  25. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  26. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  27. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  28. package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
  29. package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
  30. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  31. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  32. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  33. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  34. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  35. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  36. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  37. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  38. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  39. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
  40. package/contracts/next-development-request-v1.schema.json +66 -0
  41. package/contracts/next-development-transition-v1.schema.json +292 -0
  42. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  43. package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
  44. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  45. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  46. package/contracts/v4-tail-reseal-v1.schema.json +276 -0
  47. package/dist/site/agent-index.json +4 -0
  48. package/dist/site/artifact-schemas.json +8 -0
  49. package/dist/site/buildchain-contract.json +1393 -28
  50. package/dist/site/buildchain-site.json +477 -32
  51. package/dist/site/capability-registry.json +17 -14
  52. package/dist/site/cli-registry.json +159 -1
  53. package/dist/site/kfd-claims.json +455 -15
  54. package/dist/site/kfd-upstream-aggregate.json +1 -1
  55. package/dist/site/manual-registry.json +53 -7
  56. package/dist/site/node-api-registry.json +14008 -9602
  57. package/dist/site/page-registry.json +442 -21
  58. package/dist/site/public-surface-audit.json +440 -11
  59. package/dist/site/publication-authority-registry.json +44 -8
  60. package/dist/site/publication-registry.json +4 -4
  61. package/dist/site/release-provenance.json +22 -0
  62. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  63. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  64. package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
  65. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  66. package/dist/site/site-manifest.json +35 -11
  67. package/dist/site/workflow-registry.json +142 -8
  68. package/docs/MAP.md +7 -2
  69. package/docs/cli-reference.md +193 -0
  70. package/docs/cli.md +8 -0
  71. package/docs/dev-alpha-candidate-patrol.md +8 -0
  72. package/docs/dev-delivery-warrant.md +22 -1
  73. package/docs/next-development-transition.md +118 -0
  74. package/docs/node-api-reference.md +583 -288
  75. package/docs/release-candidate.md +7 -0
  76. package/docs/release-tail-provider-plane.md +30 -0
  77. package/docs/v4-adopter-delivery.md +133 -0
  78. package/docs/v4-compatibility-facts.md +91 -0
  79. package/docs/v4-publication-rehearsal.md +130 -0
  80. package/docs/v4-tail-reseal.md +73 -0
  81. package/docs/versioning.md +4 -3
  82. package/package.json +30 -6
  83. package/packages/core/adopter-delivery-passport.js +259 -0
  84. package/packages/core/adopter-delivery-vectors.js +158 -0
  85. package/packages/core/adopter-delivery-vectors.json +77 -0
  86. package/packages/core/buildchain-agent-manuals.js +3 -0
  87. package/packages/core/buildchain-compatibility-authority.js +336 -0
  88. package/packages/core/buildchain-compatibility-fact.js +394 -0
  89. package/packages/core/buildchain-compatibility-facts.json +217 -0
  90. package/packages/core/buildchain-compatibility-proof.js +566 -0
  91. package/packages/core/buildchain-config.js +84 -0
  92. package/packages/core/buildchain-contract.js +93 -141
  93. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  94. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  95. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  96. package/packages/core/buildchain-publication-authority.js +1 -0
  97. package/packages/core/channel-candidate.js +8 -0
  98. package/packages/core/channel-promotion-baseline.js +55 -0
  99. package/packages/core/ci-lane-change-budget.js +247 -0
  100. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  101. package/packages/core/dev-delivery-execution-transfer.js +21 -1
  102. package/packages/core/dev-delivery-native-proof.js +43 -27
  103. package/packages/core/dev-delivery-process-boundary.js +19 -6
  104. package/packages/core/dev-delivery-provider-heartbeat.js +18 -6
  105. package/packages/core/dev-delivery-warrant-settlement.js +234 -2
  106. package/packages/core/dev-delivery-warrant-state.js +46 -10
  107. package/packages/core/dev-delivery-warrant.js +16 -3
  108. package/packages/core/github-governance-authority.js +1 -0
  109. package/packages/core/index.js +5 -0
  110. package/packages/core/kungfu-temporal-fact.js +557 -0
  111. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  112. package/packages/core/next-development-candidate-reservation.js +186 -0
  113. package/packages/core/next-development-controller.js +728 -0
  114. package/packages/core/next-development-projection.js +287 -0
  115. package/packages/core/next-development-transition.js +738 -0
  116. package/packages/core/paper-agent-entry.js +11 -5
  117. package/packages/core/paper.js +16 -5
  118. package/packages/core/published-delivery-authority.js +266 -0
  119. package/packages/core/release-candidate-recovery.js +70 -15
  120. package/packages/core/release-candidate.js +2 -2
  121. package/packages/core/release-passport.js +36 -9
  122. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  123. package/packages/core/v4-adopter-delivery.js +359 -0
  124. package/packages/core/v4-canonical-contracts.js +5 -0
  125. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  126. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  127. package/packages/core/v4-floating-consumer-policy.js +143 -20
  128. package/packages/core/v4-floating-consumer-release-passport.js +35 -8
  129. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  130. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  131. package/packages/core/v4-publication-rehearsal.js +481 -0
  132. package/packages/core/v4-tail-reseal-contract.js +26 -0
  133. package/packages/core/v4-tail-reseal-github.js +178 -0
  134. package/packages/core/v4-tail-reseal-receipt.js +225 -0
  135. package/packages/core/v4-tail-reseal.js +594 -0
  136. package/packages/core/workflow-yaml-contract.js +24 -2
  137. package/scripts/artifact-signing-controller-core.mjs +1 -1
  138. package/scripts/buildchain-cli-help.mjs +30 -0
  139. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  140. package/scripts/check-inventory.mjs +14 -3
  141. package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
  142. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  143. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  144. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  145. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  146. package/scripts/dev-delivery-warrant-options.mjs +27 -11
  147. package/scripts/dev-delivery-warrant-store.mjs +10 -6
  148. package/scripts/dev-delivery-warrant.mjs +50 -6
  149. package/scripts/dev-pr-auto-merge.mjs +25 -11
  150. package/scripts/dev-pr-delivery-warrant.mjs +45 -0
  151. package/scripts/dev-pr-prequeue-guard.mjs +0 -18
  152. package/scripts/dev-qualification-patrol.mjs +9 -9
  153. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  154. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  155. package/scripts/generate-next-development-guidance.mjs +49 -0
  156. package/scripts/generate-release-candidate-passport.mjs +152 -0
  157. package/scripts/generate-site-bundle.mjs +46 -3
  158. package/scripts/init-repo.mjs +22 -4
  159. package/scripts/next-development-self-dogfood-harness.mjs +460 -0
  160. package/scripts/next-development-self-dogfood.mjs +597 -0
  161. package/scripts/next-development-transition.mjs +47 -0
  162. package/scripts/project-cut-merge-queue-admission.mjs +394 -0
  163. package/scripts/promotion-channel-router.mjs +4 -4
  164. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  165. package/scripts/release-tail.mjs +51 -3
  166. package/scripts/resume-from-candidate-run.mjs +595 -65
  167. package/scripts/site-capability-metadata.mjs +29 -0
  168. package/scripts/stable-candidate-qualification.mjs +9 -10
  169. package/scripts/v4-consumer-policy.mjs +5 -5
  170. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
  171. package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
  172. package/scripts/v4-tail-reseal.mjs +416 -0
@@ -0,0 +1,247 @@
1
+ import crypto from "node:crypto";
2
+ import { parseWorkflowDocument } from "./workflow-yaml-contract.js";
3
+
4
+ export const CI_LANE_CHANGE_BUDGET_CONTRACT =
5
+ "buildchain.ci-lane-change-budget/v1";
6
+
7
+ const AUTHORITY_CLASSES = new Set([
8
+ "merge-critical-required",
9
+ "merge-critical-admission",
10
+ "post-merge-advisory",
11
+ "diagnostic",
12
+ "evidence",
13
+ "release",
14
+ ]);
15
+ const TRIGGER_CLASSES = new Set([
16
+ "merge-group",
17
+ "pull-request",
18
+ "push",
19
+ "scheduled",
20
+ "manual",
21
+ "reusable",
22
+ "release",
23
+ "mixed",
24
+ ]);
25
+ const CONCURRENCY_MODES = new Set([
26
+ "none",
27
+ "ref-scoped",
28
+ "workflow-scoped",
29
+ "serialized",
30
+ ]);
31
+ const CANCELLATION_BEHAVIORS = new Set([
32
+ "cancel-stale",
33
+ "finish-started",
34
+ "provider-default",
35
+ "not-applicable",
36
+ ]);
37
+
38
+ function canonical(value) {
39
+ if (Array.isArray(value)) return value.map(canonical);
40
+ if (value && typeof value === "object") {
41
+ return Object.fromEntries(
42
+ Object.keys(value)
43
+ .sort()
44
+ .map((key) => [key, canonical(value[key])]),
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+
50
+ function root(value) {
51
+ return `sha256:${crypto
52
+ .createHash("sha256")
53
+ .update(JSON.stringify(canonical(value)))
54
+ .digest("hex")}`;
55
+ }
56
+
57
+ function triggerClass(triggers) {
58
+ const classes = new Set();
59
+ for (const trigger of triggers) {
60
+ if (trigger === "merge_group") classes.add("merge-group");
61
+ else if (trigger.startsWith("pull_request")) classes.add("pull-request");
62
+ else if (trigger === "push") classes.add("push");
63
+ else if (trigger === "schedule") classes.add("scheduled");
64
+ else if (trigger === "workflow_dispatch") classes.add("manual");
65
+ else if (trigger === "workflow_call") classes.add("reusable");
66
+ else if (trigger === "release") classes.add("release");
67
+ else classes.add("mixed");
68
+ }
69
+ return classes.size === 1 ? [...classes][0] : "mixed";
70
+ }
71
+
72
+ function diagnostic(code, laneId, message) {
73
+ return { code, laneId, message };
74
+ }
75
+
76
+ function validateDeclaration(declaration, observed) {
77
+ const laneId = String(declaration?.laneId || "");
78
+ const diagnostics = [];
79
+ if (!AUTHORITY_CLASSES.has(declaration?.authorityClass)) {
80
+ diagnostics.push(
81
+ diagnostic(
82
+ "invalid-authority-class",
83
+ laneId,
84
+ "authorityClass must identify the lane's merge or non-merge authority",
85
+ ),
86
+ );
87
+ }
88
+ if (!TRIGGER_CLASSES.has(declaration?.triggerClass)) {
89
+ diagnostics.push(
90
+ diagnostic(
91
+ "invalid-trigger-class",
92
+ laneId,
93
+ "triggerClass must identify the workflow trigger class",
94
+ ),
95
+ );
96
+ } else if (observed && declaration.triggerClass !== observed.triggerClass) {
97
+ diagnostics.push(
98
+ diagnostic(
99
+ "trigger-class-mismatch",
100
+ laneId,
101
+ `declared ${declaration.triggerClass}, observed ${observed.triggerClass}`,
102
+ ),
103
+ );
104
+ }
105
+ const concurrency = declaration?.concurrencyPolicy;
106
+ if (
107
+ !concurrency ||
108
+ !CONCURRENCY_MODES.has(concurrency.mode) ||
109
+ typeof concurrency.cancelInProgress !== "boolean"
110
+ ) {
111
+ diagnostics.push(
112
+ diagnostic(
113
+ "invalid-concurrency-policy",
114
+ laneId,
115
+ "concurrencyPolicy requires a known mode and boolean cancelInProgress",
116
+ ),
117
+ );
118
+ }
119
+ if (
120
+ !Number.isFinite(declaration?.expectedRunnerMinutes) ||
121
+ declaration.expectedRunnerMinutes <= 0
122
+ ) {
123
+ diagnostics.push(
124
+ diagnostic(
125
+ "invalid-runner-budget",
126
+ laneId,
127
+ "expectedRunnerMinutes must be greater than zero",
128
+ ),
129
+ );
130
+ }
131
+ if (!CANCELLATION_BEHAVIORS.has(declaration?.cancellationBehavior)) {
132
+ diagnostics.push(
133
+ diagnostic(
134
+ "invalid-cancellation-behavior",
135
+ laneId,
136
+ "cancellationBehavior must describe how stale or started work settles",
137
+ ),
138
+ );
139
+ }
140
+ const impact = declaration?.sloImpact;
141
+ if (
142
+ !impact ||
143
+ typeof impact.mergeCritical !== "boolean" ||
144
+ !String(impact.metric || "").trim() ||
145
+ !Number.isFinite(impact.expectedContributionSeconds) ||
146
+ impact.expectedContributionSeconds < 0 ||
147
+ !String(impact.rationale || "").trim()
148
+ ) {
149
+ diagnostics.push(
150
+ diagnostic(
151
+ "invalid-slo-impact",
152
+ laneId,
153
+ "sloImpact requires mergeCritical, metric, expectedContributionSeconds, and rationale",
154
+ ),
155
+ );
156
+ }
157
+ return diagnostics;
158
+ }
159
+
160
+ function collectLanes(workflows) {
161
+ const lanes = new Map();
162
+ for (const workflow of workflows) {
163
+ const workflowPath = String(workflow?.path || "").trim();
164
+ if (!workflowPath) throw new Error("workflow path is required");
165
+ const document = parseWorkflowDocument(workflow.text);
166
+ for (const job of document.jobs) {
167
+ const laneId = `${workflowPath}#${job.id}`;
168
+ if (lanes.has(laneId)) throw new Error(`duplicate CI lane: ${laneId}`);
169
+ lanes.set(laneId, {
170
+ laneId,
171
+ workflowPath,
172
+ jobId: job.id,
173
+ triggerClass: triggerClass(document.triggers),
174
+ });
175
+ }
176
+ }
177
+ return lanes;
178
+ }
179
+
180
+ export function evaluateCiLaneChangeBudget({
181
+ policy,
182
+ workflows,
183
+ baselineWorkflows = null,
184
+ } = {}) {
185
+ if (policy?.schema !== CI_LANE_CHANGE_BUDGET_CONTRACT) {
186
+ throw new Error(`expected ${CI_LANE_CHANGE_BUDGET_CONTRACT}`);
187
+ }
188
+ if (!Array.isArray(workflows)) throw new Error("workflows must be an array");
189
+ const observed = collectLanes(workflows);
190
+ const baselineLaneIds = baselineWorkflows
191
+ ? [...collectLanes(baselineWorkflows).keys()]
192
+ : policy.baseline?.lanes || [];
193
+ const baseline = new Set(baselineLaneIds);
194
+ if (baseline.size !== baselineLaneIds.length) {
195
+ throw new Error("baseline CI lanes must be unique");
196
+ }
197
+ const declarations = new Map();
198
+ const diagnostics = [];
199
+ for (const declaration of policy.declarations || []) {
200
+ const laneId = String(declaration?.laneId || "").trim();
201
+ if (!laneId) throw new Error("CI lane declaration laneId is required");
202
+ if (declarations.has(laneId))
203
+ throw new Error(`duplicate CI lane declaration: ${laneId}`);
204
+ declarations.set(laneId, declaration);
205
+ if (!observed.has(laneId)) {
206
+ diagnostics.push(
207
+ diagnostic(
208
+ "stale-declaration",
209
+ laneId,
210
+ "declared CI lane is absent from the current workflow cut",
211
+ ),
212
+ );
213
+ }
214
+ diagnostics.push(
215
+ ...validateDeclaration(declaration, observed.get(laneId) || null),
216
+ );
217
+ }
218
+ const newLanes = [...observed.keys()]
219
+ .filter((laneId) => !baseline.has(laneId))
220
+ .sort();
221
+ for (const laneId of newLanes) {
222
+ if (!declarations.has(laneId)) {
223
+ diagnostics.push(
224
+ diagnostic(
225
+ "undeclared-new-lane",
226
+ laneId,
227
+ "new CI lane must declare authority, trigger, concurrency, runner, cancellation, and SLO budgets",
228
+ ),
229
+ );
230
+ }
231
+ }
232
+ const result = {
233
+ schema: "buildchain.ci-lane-change-budget.evaluation/v1",
234
+ ok: diagnostics.length === 0,
235
+ observedLaneCount: observed.size,
236
+ baselineLaneCount: baseline.size,
237
+ newLaneCount: newLanes.length,
238
+ declaredLaneCount: declarations.size,
239
+ newLanes,
240
+ diagnostics: diagnostics.sort((left, right) =>
241
+ `${left.laneId}:${left.code}`.localeCompare(
242
+ `${right.laneId}:${right.code}`,
243
+ ),
244
+ ),
245
+ };
246
+ return { ...result, evaluationRoot: root(result) };
247
+ }
@@ -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 ||
@@ -43,7 +43,39 @@ export const DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA =
43
43
  export const NATIVE_PROOF_BASE_DELTA_SCHEMA =
44
44
  "kungfu.buildchain.native-proof-base-delta/v1";
45
45
  const NATIVE_QUALIFICATION_ROOT_SEMANTICS = "semantic-native-identity-v1";
46
- export function validateActiveDevDeliveryWarrant(queue) {
46
+
47
+ function normalizeQualifiedWarrant(warrant, candidate, allowLegacyV3Readback) {
48
+ const qualifiedV3Readback =
49
+ allowLegacyV3Readback &&
50
+ !warrant.nativeCommandContract &&
51
+ !candidate.nativeCommandContract &&
52
+ !warrant.nativeExecutionReceiptRoot &&
53
+ !warrant.qualificationReceiptRoot;
54
+ warrant.nativeProofRoot = exactRoot(
55
+ warrant.nativeProofRoot,
56
+ "Warrant nativeProofRoot",
57
+ );
58
+ warrant.nativeProofReuseRoot = exactRoot(
59
+ warrant.nativeProofReuseRoot,
60
+ "Warrant nativeProofReuseRoot",
61
+ );
62
+ if (!qualifiedV3Readback) {
63
+ warrant.nativeExecutionReceiptRoot = exactRoot(
64
+ warrant.nativeExecutionReceiptRoot,
65
+ "Warrant nativeExecutionReceiptRoot",
66
+ );
67
+ warrant.qualificationReceiptRoot = exactRoot(
68
+ warrant.qualificationReceiptRoot,
69
+ "Warrant qualificationReceiptRoot",
70
+ );
71
+ }
72
+ warrant.qualifiedAt = timestamp(warrant.qualifiedAt, "Warrant qualifiedAt");
73
+ }
74
+
75
+ export function validateActiveDevDeliveryWarrant(
76
+ queue,
77
+ { allowLegacyV3Readback = false } = {},
78
+ ) {
47
79
  const warrant = queue.activeWarrant;
48
80
  if (warrant.schema !== "kungfu.buildchain.dev-delivery-warrant/v1")
49
81
  throw new Error("active Warrant schema is unsupported");
@@ -68,24 +100,17 @@ export function validateActiveDevDeliveryWarrant(queue) {
68
100
  const phase = historicalPhaseLess ? "legacy-active" : warrant.phase;
69
101
  if (!["legacy-active", "provisional", "qualified"].includes(phase))
70
102
  throw new Error("active Warrant phase is unsupported");
71
- if (warrant.phase === "qualified") {
72
- warrant.nativeProofRoot = exactRoot(
73
- warrant.nativeProofRoot,
74
- "Warrant nativeProofRoot",
75
- );
76
- warrant.nativeProofReuseRoot = exactRoot(
77
- warrant.nativeProofReuseRoot,
78
- "Warrant nativeProofReuseRoot",
79
- );
80
- warrant.nativeExecutionReceiptRoot = exactRoot(
81
- warrant.nativeExecutionReceiptRoot,
82
- "Warrant nativeExecutionReceiptRoot",
83
- );
84
- warrant.qualificationReceiptRoot = exactRoot(
85
- warrant.qualificationReceiptRoot,
86
- "Warrant qualificationReceiptRoot",
103
+ const active = queue.candidates.filter((candidate) =>
104
+ ["selected", "proving", "waiting", "blocked", "qualified"].includes(
105
+ candidate.status,
106
+ ),
107
+ );
108
+ if (active.length !== 1 || active[0].candidateId !== warrant.candidateId)
109
+ throw new Error(
110
+ "exactly one active candidate must match the active Warrant",
87
111
  );
88
- warrant.qualifiedAt = timestamp(warrant.qualifiedAt, "Warrant qualifiedAt");
112
+ if (warrant.phase === "qualified") {
113
+ normalizeQualifiedWarrant(warrant, active[0], allowLegacyV3Readback);
89
114
  } else if (
90
115
  warrant.phase === "provisional" &&
91
116
  (warrant.nativeProofRoot ||
@@ -98,15 +123,6 @@ export function validateActiveDevDeliveryWarrant(queue) {
98
123
  "provisional Warrant cannot carry qualified native evidence",
99
124
  );
100
125
  }
101
- const active = queue.candidates.filter((candidate) =>
102
- ["selected", "proving", "waiting", "blocked", "qualified"].includes(
103
- candidate.status,
104
- ),
105
- );
106
- if (active.length !== 1 || active[0].candidateId !== warrant.candidateId)
107
- throw new Error(
108
- "exactly one active candidate must match the active Warrant",
109
- );
110
126
  if (
111
127
  historicalPhaseLess &&
112
128
  (active[0].environmentRoot ||
@@ -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
  }