@kungfu-tech/buildchain 4.0.2-alpha.5 → 4.0.2-alpha.50

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 (171) hide show
  1. package/AGENTS.md +10 -1
  2. package/CONTRIBUTING.md +24 -0
  3. package/README.md +2 -2
  4. package/actions/promote-buildchain-ref/README.md +1 -1
  5. package/actions/v4-release-candidate-promote/README.md +28 -0
  6. package/architecture/agent-change-map.md +70 -9
  7. package/architecture/ci-lane-change-budget.json +384 -0
  8. package/architecture/internal-capabilities.json +56 -9
  9. package/architecture/maintainability-debt.json +162 -107
  10. package/architecture/maintainability-policy.json +34 -30
  11. package/architecture/release-tail-contract-inventory.json +6 -6
  12. package/architecture/v4-architecture-constitution.md +23 -20
  13. package/architecture/v4-cross-platform-adopter-qualification.json +1 -1
  14. package/architecture/v4-delivery-authority-parity.json +2 -2
  15. package/architecture/v4-delivery-warrant-shadow-bootstrap-plan.json +1 -1
  16. package/architecture/v4-floating-consumer-policy.json +2 -2
  17. package/architecture/v4-next-development-parity.json +5 -5
  18. package/architecture/v4-partial-mutation-recovery-qualification.json +4 -1
  19. package/architecture/v4-platform-stage-checkpoints.json +1 -1
  20. package/architecture/v4-provider-operation-journal-contract.json +5 -2
  21. package/architecture/v4-publication-facade-patches.json +64 -0
  22. package/architecture/v4-release-activation-shadow-domain.json +5 -2
  23. package/architecture/v4-release-topology.json +198 -38
  24. package/architecture/v4-rust-wasm-production-authority.json +58 -0
  25. package/architecture/v4-stable-publication-fence.json +5 -2
  26. package/architecture/v4-stage-capsule-qualification.json +2 -2
  27. package/architecture/v4-tail-reseal-parity.json +8 -6
  28. package/architecture/v4-universal-workflow-bootstrap.json +176 -0
  29. package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
  30. package/architecture/v4-universal-workflow-train-admission.json +40 -0
  31. package/architecture/workflow-taxonomy.json +1462 -0
  32. package/bin/buildchain.mjs +7 -7
  33. package/contracts/buildchain-v2-residuals-v1.json +57 -48
  34. package/contracts/dev-delivery-authority-v2.schema.json +16 -2
  35. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  36. package/contracts/v4-canonical-contracts-v1.schema.json +1 -0
  37. package/dist/site/buildchain-contract.json +93 -48
  38. package/dist/site/buildchain-site.json +276 -72
  39. package/dist/site/capability-registry.json +5 -5
  40. package/dist/site/controller-registry.json +50 -13
  41. package/dist/site/kfd-claims.json +2610 -983
  42. package/dist/site/kfd-upstream-aggregate.json +1 -1
  43. package/dist/site/manual-registry.json +14 -14
  44. package/dist/site/node-api-registry.json +273 -127
  45. package/dist/site/page-registry.json +257 -53
  46. package/dist/site/public-surface-audit.json +3239 -1232
  47. package/dist/site/publication-authority-registry.json +1135 -363
  48. package/dist/site/publication-registry.json +4 -4
  49. package/dist/site/release-provenance.json +2 -1
  50. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +16 -2
  51. package/dist/site/site-manifest.json +18 -18
  52. package/dist/site/workflow-registry.json +5097 -1659
  53. package/docs/MAP.md +2 -0
  54. package/docs/auditable-demo.md +1 -1
  55. package/docs/cli.md +21 -8
  56. package/docs/dev-delivery-warrant.md +1 -1
  57. package/docs/engineering-housekeeper.md +3 -3
  58. package/docs/kfd-support.md +26 -2
  59. package/docs/next-development-transition.md +2 -2
  60. package/docs/node-api-reference.md +216 -205
  61. package/docs/oci-publication.md +160 -0
  62. package/docs/publication-authority.md +2 -2
  63. package/docs/publish-transaction.md +36 -3
  64. package/docs/release-candidate.md +3 -3
  65. package/docs/release-flow.md +67 -19
  66. package/docs/release-governance.md +8 -8
  67. package/docs/release-passport.md +27 -2
  68. package/docs/reusable-build-surface.md +16 -0
  69. package/docs/v4-adopter-delivery.md +2 -2
  70. package/docs/v4-production-release.md +71 -8
  71. package/docs/v4-rust-wasm-production-authority.md +58 -0
  72. package/docs/v4-stage-capsule.md +2 -2
  73. package/docs/v4-tail-reseal.md +1 -1
  74. package/docs/workflow-catalog.md +155 -0
  75. package/docs/workflow-path-migration.md +109 -0
  76. package/package.json +12 -7
  77. package/packages/core/artifact-verification-envelope.js +5 -5
  78. package/packages/core/buildchain-contract.js +1 -1
  79. package/packages/core/buildchain-kfd-claims.js +2 -2
  80. package/packages/core/buildchain-publication-authority.js +43 -26
  81. package/packages/core/buildchain-v4-domain.wasm +0 -0
  82. package/packages/core/ci-lane-change-budget.js +14 -1
  83. package/packages/core/controller-evidence.js +4 -1
  84. package/packages/core/dev-delivery-candidate-identity.js +40 -6
  85. package/packages/core/dev-delivery-warrant-state.js +3 -4
  86. package/packages/core/dev-delivery-warrant.js +12 -2
  87. package/packages/core/github-governance-authority.js +4 -4
  88. package/packages/core/kfd-adopter-manifest.js +46 -47
  89. package/packages/core/next-development-projection.js +2 -2
  90. package/packages/core/next-development-transition.js +5 -6
  91. package/packages/core/oci-compose-qualification.js +140 -0
  92. package/packages/core/oci-publication-bundle.js +281 -0
  93. package/packages/core/oci-publication-graph.js +206 -0
  94. package/packages/core/paper-agent-entry.js +30 -4
  95. package/packages/core/paper-fleet.js +1 -1
  96. package/packages/core/paper-runtime-channels.js +348 -0
  97. package/packages/core/paper-scaffold-content.js +10 -0
  98. package/packages/core/paper.js +37 -71
  99. package/packages/core/publication-sealed-bundle.js +78 -13
  100. package/packages/core/release-line-dry-run.js +1 -1
  101. package/packages/core/release-passport-contract.js +5 -5
  102. package/packages/core/release-passport.js +5 -4
  103. package/packages/core/release-tail-product-capabilities.js +38 -0
  104. package/packages/core/release-tail-provider-plane.js +68 -1048
  105. package/packages/core/v4-canonical-contracts.js +15 -75
  106. package/packages/core/v4-domain-wasm-artifact.js +7 -0
  107. package/packages/core/v4-domain-wasm.js +216 -0
  108. package/packages/core/v4-floating-consumer-evidence.js +1 -1
  109. package/packages/core/v4-floating-consumer-policy.js +1 -1
  110. package/packages/core/v4-partial-mutation-recovery-qualification.js +9 -386
  111. package/packages/core/v4-product-publication.js +59 -0
  112. package/packages/core/v4-protected-publication-source.js +19 -21
  113. package/packages/core/v4-provider-operation-journal.js +18 -487
  114. package/packages/core/v4-provider-readback-idempotency.js +12 -97
  115. package/packages/core/v4-release-activation-shadow.js +13 -500
  116. package/packages/core/v4-release-invocation.js +28 -389
  117. package/packages/core/v4-stable-publication-fence.js +13 -342
  118. package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
  119. package/scripts/binary-publication-evidence.mjs +135 -0
  120. package/scripts/build-contract-core.mjs +1 -14
  121. package/scripts/build-v4-domain-wasm.mjs +189 -0
  122. package/scripts/check-action-bundles.mjs +12 -2
  123. package/scripts/check-ci-lane-change-budget.mjs +4 -1
  124. package/scripts/check-core-mechanism-inventory.mjs +4 -3
  125. package/scripts/check-inventory.mjs +35 -44
  126. package/scripts/check-maintainability.mjs +16 -113
  127. package/scripts/check-v4-floating-consumer-policy-contract.mjs +2 -2
  128. package/scripts/check-v4-public-dogfood-contract.mjs +22 -9
  129. package/scripts/check-v4-release-topology.mjs +236 -50
  130. package/scripts/check-v4-runtime-semantic-closure.mjs +4 -1
  131. package/scripts/check-v4-universal-workflow-bootstrap.mjs +266 -0
  132. package/scripts/check-workflow-taxonomy.mjs +6 -0
  133. package/scripts/copy-v4-domain-wasm.mjs +14 -0
  134. package/scripts/dev-delivery-authority-command-adapters.mjs +1 -1
  135. package/scripts/dev-delivery-authority.mjs +8 -8
  136. package/scripts/dev-delivery-source-proof-reuse.mjs +2 -1
  137. package/scripts/dev-delivery-warrant-options.mjs +14 -9
  138. package/scripts/dev-delivery-warrant-store.mjs +73 -7
  139. package/scripts/dev-delivery-warrant-transition.mjs +109 -0
  140. package/scripts/dev-delivery-warrant.mjs +76 -99
  141. package/scripts/dev-pr-auto-merge.mjs +15 -15
  142. package/scripts/generate-channel-build-workflow.mjs +7 -2
  143. package/scripts/generate-channel-promotion-workflow.mjs +10 -25
  144. package/scripts/generate-site-bundle.mjs +11 -11
  145. package/scripts/generate-v4-universal-workflow-facades.mjs +497 -0
  146. package/scripts/generate-workflow-taxonomy.mjs +25 -0
  147. package/scripts/github-output.mjs +31 -0
  148. package/scripts/maintainability-metrics.mjs +16 -5
  149. package/scripts/maintainability-public-surface.mjs +115 -0
  150. package/scripts/next-development-review.mjs +307 -0
  151. package/scripts/oci-compose-preview.mjs +250 -0
  152. package/scripts/paper-work-fleet-cli.mjs +2 -0
  153. package/scripts/publication-candidate-kind.mjs +93 -0
  154. package/scripts/publication-candidate-sealer.mjs +16 -1
  155. package/scripts/release-asset-client.mjs +91 -0
  156. package/scripts/release-candidate-resolver.mjs +23 -35
  157. package/scripts/resume-from-candidate-run.mjs +11 -18
  158. package/scripts/site-capability-metadata.mjs +1 -0
  159. package/scripts/source-verification-evidence.mjs +344 -0
  160. package/scripts/stable-candidate-qualification.mjs +1 -1
  161. package/scripts/universal-facade-maintainability.mjs +91 -0
  162. package/scripts/v4-product-publication-intent.mjs +129 -0
  163. package/scripts/v4-publication-settlement.mjs +211 -0
  164. package/scripts/v4-universal-workflow-backflow.mjs +212 -0
  165. package/scripts/v4-universal-workflow-engine.mjs +620 -0
  166. package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
  167. package/scripts/verify-version-state-delta.mjs +173 -0
  168. package/scripts/workflow-taxonomy.mjs +507 -0
  169. package/templates/native-dev-delivery.yml +4 -7
  170. package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
  171. package/templates/universal-buildchain-bootstrap.yml +28 -0
@@ -0,0 +1,140 @@
1
+ import { v4ContentRoot } from "./v4-canonical-contracts.js";
2
+ import { releaseTailRoot } from "./release-tail-provider-plane.js";
3
+ import { ociPublicationTag } from "./oci-publication-graph.js";
4
+
5
+ function check(condition, message) {
6
+ if (!condition) throw new Error(`Compose preview: ${message}`);
7
+ }
8
+
9
+ export function verifyComposePublication({
10
+ family,
11
+ readback,
12
+ documents,
13
+ repository,
14
+ tag,
15
+ }) {
16
+ const { root, ...body } = family;
17
+ const version = tag.replace(/^v/u, "");
18
+ const operation = documents.productState.operations.find(
19
+ (entry) => entry.capabilityId === "product.oci.publish",
20
+ );
21
+ check(
22
+ family.schema === "kungfu-buildchain-oci-family/v2" &&
23
+ root === v4ContentRoot("oci-publication-family", body) &&
24
+ family.repository === repository &&
25
+ family.version === version &&
26
+ family.sourceSha === documents.invocation.candidate.commit &&
27
+ readback.familyRoot === root &&
28
+ readback.version === version &&
29
+ readback.candidateSourceSha === family.sourceSha &&
30
+ operation?.receipt.evidenceRoots.includes(releaseTailRoot(readback)),
31
+ "unbound public OCI family",
32
+ );
33
+ const applications = family.images.filter(
34
+ (entry) => entry.kind === "compose" && entry.preview,
35
+ );
36
+ check(applications.length === 1, "expected one declared Compose preview");
37
+ const application = applications[0],
38
+ policy = application.preview;
39
+ check(
40
+ policy.alias === "compose-preview" &&
41
+ /^\.github\/workflows\/[a-z0-9-]+\.yml$/u.test(
42
+ policy.qualificationWorkflow,
43
+ ) &&
44
+ /^(none|sha256:[0-9a-f]{64})$/u.test(policy.previousDigest),
45
+ "invalid preview policy",
46
+ );
47
+ const image = family.images.find(
48
+ (entry) =>
49
+ entry.name === application.targetImage && entry.kind !== "compose",
50
+ );
51
+ check(
52
+ image &&
53
+ application.repository === image.repository &&
54
+ image.repository === `ghcr.io/${repository}/${image.name}` &&
55
+ /^[a-z0-9][a-z0-9._-]*$/u.test(image.name),
56
+ "foreign application destination",
57
+ );
58
+ for (const entry of [image, application])
59
+ check(
60
+ readback.images.some(
61
+ (observed) =>
62
+ observed.name === entry.name &&
63
+ observed.repository === entry.repository &&
64
+ observed.digest === entry.digest &&
65
+ observed.ref === ociPublicationTag(entry, version) &&
66
+ observed.anonymous === true,
67
+ ),
68
+ "missing public artifact readback",
69
+ );
70
+ return {
71
+ application,
72
+ image,
73
+ policy,
74
+ familyRoot: root,
75
+ repository,
76
+ tag,
77
+ sourceSha: documents.product.publication.releaseSha,
78
+ publicationReceiptRoot: documents.receipt.receiptRoot,
79
+ };
80
+ }
81
+
82
+ export function verifyComposeQualification(context, run, receipt) {
83
+ const { application, image, policy, repository, tag, sourceSha, familyRoot } =
84
+ context;
85
+ check(
86
+ run.repository?.full_name === repository &&
87
+ run.head_repository?.full_name === repository &&
88
+ run.event === "workflow_dispatch" &&
89
+ run.status === "completed" &&
90
+ run.conclusion === "success" &&
91
+ run.head_sha === sourceSha &&
92
+ run.path.split("@")[0] === policy.qualificationWorkflow,
93
+ "qualification run is not the exact published source",
94
+ );
95
+ if (!receipt) return;
96
+ check(
97
+ receipt.schema === "kungfu-buildchain-compose-qualification/v1" &&
98
+ receipt.repository === repository &&
99
+ receipt.tag === tag &&
100
+ receipt.sourceSha === sourceSha &&
101
+ receipt.familyRoot === familyRoot &&
102
+ String(receipt.runId) === String(run.id) &&
103
+ Number(receipt.runAttempt) === run.run_attempt &&
104
+ receipt.image === `${image.repository}@${image.digest}` &&
105
+ receipt.application ===
106
+ `${application.repository}@${application.digest}` &&
107
+ receipt.previousDigest === policy.previousDigest &&
108
+ receipt.passed === true,
109
+ "qualification receipt identity mismatch",
110
+ );
111
+ for (const name of [
112
+ "freshInstall",
113
+ "restartPersistence",
114
+ "upgradePersistence",
115
+ "rollbackPersistence",
116
+ "accountIsolation",
117
+ "hardenedRuntime",
118
+ ])
119
+ check(receipt.checks?.[name] === true, `qualification lacks ${name}`);
120
+ check(
121
+ Array.isArray(image.platforms) &&
122
+ image.platforms.length === 2 &&
123
+ JSON.stringify([...image.platforms].sort()) ===
124
+ JSON.stringify(["linux/amd64", "linux/arm64"]) &&
125
+ image.platforms.every(
126
+ (platform) => receipt.platforms?.[platform]?.passed === true,
127
+ ),
128
+ "qualification lacks both platforms",
129
+ );
130
+ check(
131
+ Array.isArray(receipt.evidence) &&
132
+ receipt.evidence.length > 0 &&
133
+ receipt.evidence.every(
134
+ (entry) =>
135
+ /^[a-zA-Z0-9][a-zA-Z0-9._-]*\.json$/u.test(entry.path) &&
136
+ /^sha256:[0-9a-f]{64}$/u.test(entry.sha256),
137
+ ),
138
+ "missing qualification evidence bindings",
139
+ );
140
+ }
@@ -0,0 +1,281 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { v4ContentRoot } from "./v4-canonical-contracts.js";
5
+ import {
6
+ verifyOciGraph,
7
+ verifyOciDestination,
8
+ } from "./oci-publication-graph.js";
9
+
10
+ export const OCI_FAMILY_SCHEMA = "kungfu-buildchain-oci-family/v1";
11
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
12
+ const namePattern = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;
13
+
14
+ export function sealOciPublicationBundle({ bundleRoot, body }) {
15
+ const manifest = {
16
+ ...body,
17
+ root: v4ContentRoot("oci-publication-family", body),
18
+ };
19
+ verifyOciPublicationBundle({
20
+ bundleRoot,
21
+ manifest,
22
+ repository: body.repository,
23
+ sourceSha: body.sourceSha,
24
+ version: body.version,
25
+ });
26
+ return manifest;
27
+ }
28
+
29
+ function requireValue(condition, message) {
30
+ if (!condition) throw new Error(`OCI bundle: ${message}`);
31
+ }
32
+
33
+ export function ociBundleFile(root, relative) {
34
+ requireValue(
35
+ typeof relative === "string" &&
36
+ relative.length > 0 &&
37
+ !relative.includes("\\") &&
38
+ !path.isAbsolute(relative) &&
39
+ !relative.split(/[\\/]/u).some((p) => p === ".." || p === "." || !p),
40
+ "unsafe file path",
41
+ );
42
+ const base = fs.realpathSync(root);
43
+ let current = base;
44
+ for (const segment of relative.split("/")) {
45
+ current = path.join(current, segment);
46
+ requireValue(
47
+ !fs.lstatSync(current).isSymbolicLink(),
48
+ "symlinks are forbidden",
49
+ );
50
+ }
51
+ requireValue(
52
+ fs.statSync(current).isFile() &&
53
+ fs.realpathSync(current).startsWith(`${base}${path.sep}`),
54
+ "file escapes bundle",
55
+ );
56
+ return current;
57
+ }
58
+
59
+ function fileDigest(file) {
60
+ const hash = crypto.createHash("sha256"),
61
+ buffer = Buffer.alloc(1024 * 1024);
62
+ const fd = fs.openSync(file, "r");
63
+ try {
64
+ for (;;) {
65
+ const size = fs.readSync(fd, buffer);
66
+ if (!size) break;
67
+ hash.update(buffer.subarray(0, size));
68
+ }
69
+ } finally {
70
+ fs.closeSync(fd);
71
+ }
72
+ return `sha256:${hash.digest("hex")}`;
73
+ }
74
+
75
+ function verifyImageProvenance(image, config, sourceSha, version) {
76
+ const labels = config.config?.Labels || {};
77
+ requireValue(
78
+ image.content &&
79
+ /^[0-9a-f]{40}$/u.test(image.content.sourceSha) &&
80
+ typeof image.content.version === "string" &&
81
+ image.content.version.length > 0,
82
+ "missing image content provenance",
83
+ );
84
+ requireValue(
85
+ labels["org.opencontainers.image.revision"] === image.content.sourceSha &&
86
+ labels["org.opencontainers.image.version"] === image.content.version,
87
+ "image content provenance mismatch",
88
+ );
89
+ if (image.action === "built")
90
+ requireValue(
91
+ image.content.sourceSha === sourceSha &&
92
+ image.content.version === version,
93
+ "built image is not from the candidate source",
94
+ );
95
+ if (image.contractMajor !== undefined)
96
+ requireValue(
97
+ Number.isInteger(image.contractMajor) &&
98
+ image.contractMajor > 0 &&
99
+ labels["io.kungfu.image.contract-major"] ===
100
+ String(image.contractMajor),
101
+ "image contract major mismatch",
102
+ );
103
+ if (image.parentDigest)
104
+ requireValue(
105
+ digestPattern.test(image.parentDigest) &&
106
+ labels["io.kungfu.image.parent-digest"] === image.parentDigest,
107
+ "image parent digest mismatch",
108
+ );
109
+ if (image.content.materialSha)
110
+ requireValue(
111
+ /^[0-9a-f]{40}$/u.test(image.content.materialSha) &&
112
+ labels["io.kungfu.buildchain.release-material-sha"] ===
113
+ image.content.materialSha,
114
+ "image material provenance mismatch",
115
+ );
116
+ }
117
+
118
+ export function verifyOciPublicationBundle({
119
+ bundleRoot,
120
+ manifest,
121
+ repository,
122
+ sourceSha,
123
+ version,
124
+ }) {
125
+ requireValue(
126
+ [OCI_FAMILY_SCHEMA, "kungfu-buildchain-oci-family/v2"].includes(
127
+ manifest?.schema,
128
+ ),
129
+ "unsupported manifest schema",
130
+ );
131
+ const { root, ...body } = manifest;
132
+ const computed = v4ContentRoot("oci-publication-family", body);
133
+ requireValue(root === computed, "manifest root mismatch");
134
+ requireValue(
135
+ manifest.repository === repository &&
136
+ manifest.sourceSha === sourceSha &&
137
+ manifest.version === version,
138
+ "candidate identity mismatch",
139
+ );
140
+ requireValue(
141
+ /^[a-z0-9-]+\/[a-z0-9._-]+$/u.test(repository),
142
+ "invalid repository",
143
+ );
144
+ requireValue(
145
+ Array.isArray(manifest.images) && manifest.images.length > 0,
146
+ "empty image family",
147
+ );
148
+ const expected = manifest.expectedImages;
149
+ requireValue(
150
+ Array.isArray(expected) &&
151
+ expected.length === new Set(expected).size &&
152
+ expected.every((n) => namePattern.test(n)),
153
+ "invalid expected image family",
154
+ );
155
+ const actual = manifest.images.map((i) => i.name).sort();
156
+ requireValue(
157
+ JSON.stringify(actual) === JSON.stringify([...expected].sort()),
158
+ "incomplete or duplicate image family",
159
+ );
160
+ const files = new Map();
161
+ const graphs = new Map();
162
+ for (const image of manifest.images) {
163
+ verifyOciDestination(image, manifest, requireValue);
164
+ requireValue(
165
+ digestPattern.test(image.digest) &&
166
+ ["built", "reused"].includes(image.action),
167
+ "invalid image identity",
168
+ );
169
+ requireValue(
170
+ /^linux\/(amd64|arm64)$/u.test(image.platform) ||
171
+ (manifest.schema.endsWith("/v2") &&
172
+ ["multi-platform", "compose"].includes(image.platform)),
173
+ "unsupported image platform",
174
+ );
175
+ const prefix = image.layout;
176
+ const descriptorFile = (descriptor) => {
177
+ requireValue(
178
+ digestPattern.test(descriptor.digest) &&
179
+ Number.isSafeInteger(descriptor.size) &&
180
+ descriptor.size >= 0 &&
181
+ !descriptor.urls,
182
+ "invalid OCI descriptor",
183
+ );
184
+ const relative = `${prefix}/blobs/sha256/${descriptor.digest.slice(7)}`;
185
+ const file = ociBundleFile(bundleRoot, relative);
186
+ requireValue(
187
+ fs.statSync(file).size === descriptor.size,
188
+ "OCI blob size mismatch",
189
+ );
190
+ if (!files.has(file))
191
+ requireValue(
192
+ fileDigest(file) === descriptor.digest,
193
+ "OCI blob digest mismatch",
194
+ );
195
+ if (Object.hasOwn(descriptor, "data")) {
196
+ requireValue(
197
+ descriptor.size <= 1024 * 1024 &&
198
+ descriptor.data === fs.readFileSync(file).toString("base64"),
199
+ "embedded OCI content mismatch",
200
+ );
201
+ }
202
+ files.set(file, descriptor);
203
+ return file;
204
+ };
205
+ const layout = JSON.parse(
206
+ fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/oci-layout`)),
207
+ );
208
+ requireValue(
209
+ layout.imageLayoutVersion === "1.0.0",
210
+ "unsupported OCI layout",
211
+ );
212
+ const index = JSON.parse(
213
+ fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/index.json`)),
214
+ );
215
+ const selected = (index.manifests || []).filter(
216
+ (descriptor) =>
217
+ descriptor.digest === image.digest &&
218
+ (descriptor.annotations?.["org.opencontainers.image.ref.name"] ===
219
+ image.name ||
220
+ index.manifests.length === 1),
221
+ );
222
+ requireValue(
223
+ index.schemaVersion === 2 && selected.length === 1,
224
+ "OCI index must select one exact image manifest",
225
+ );
226
+ if (manifest.schema.endsWith("/v2")) {
227
+ graphs.set(
228
+ image.name,
229
+ verifyOciGraph({
230
+ image,
231
+ manifest,
232
+ descriptor: selected[0],
233
+ descriptorFile,
234
+ requireValue,
235
+ verifyProvenance: (config) =>
236
+ verifyImageProvenance(image, config, sourceSha, version),
237
+ }),
238
+ );
239
+ } else {
240
+ const document = JSON.parse(fs.readFileSync(descriptorFile(selected[0])));
241
+ requireValue(
242
+ document.schemaVersion === 2 &&
243
+ [
244
+ "application/vnd.oci.image.manifest.v1+json",
245
+ "application/vnd.docker.distribution.manifest.v2+json",
246
+ ].includes(document.mediaType) &&
247
+ Array.isArray(document.layers),
248
+ "unsupported OCI image manifest",
249
+ );
250
+ const config = JSON.parse(
251
+ fs.readFileSync(descriptorFile(document.config)),
252
+ );
253
+ requireValue(
254
+ `${config.os}/${config.architecture}` === image.platform,
255
+ "OCI platform mismatch",
256
+ );
257
+ verifyImageProvenance(image, config, sourceSha, version);
258
+ for (const layer of document.layers) descriptorFile(layer);
259
+ graphs.set(image.name, {
260
+ manifests: [selected[0]],
261
+ blobs: [document.config, ...document.layers],
262
+ });
263
+ }
264
+ const smokeFile = ociBundleFile(bundleRoot, image.smoke.path);
265
+ requireValue(
266
+ fileDigest(smokeFile) === image.smoke.sha256,
267
+ "smoke evidence digest mismatch",
268
+ );
269
+ const smoke = JSON.parse(fs.readFileSync(smokeFile));
270
+ requireValue(
271
+ smoke.passed === true && smoke.image === image.name,
272
+ "smoke qualification failed",
273
+ );
274
+ }
275
+ return {
276
+ root: computed,
277
+ manifest,
278
+ graphs,
279
+ bundleRoot: fs.realpathSync(bundleRoot),
280
+ };
281
+ }
@@ -0,0 +1,206 @@
1
+ import fs from "node:fs";
2
+
3
+ const imageTypes = [
4
+ "application/vnd.oci.image.manifest.v1+json",
5
+ "application/vnd.docker.distribution.manifest.v2+json",
6
+ ];
7
+ const indexTypes = [
8
+ "application/vnd.oci.image.index.v1+json",
9
+ "application/vnd.docker.distribution.manifest.list.v2+json",
10
+ ];
11
+ const composeType = "application/vnd.docker.compose.project";
12
+
13
+ export function ociPublicationTag(image, version) {
14
+ return `${image.kind === "compose" ? "compose-" : ""}v${version}`;
15
+ }
16
+
17
+ export function verifyOciDestination(image, manifest, requireValue) {
18
+ const compose = manifest.schema.endsWith("/v2") && image.kind === "compose";
19
+ requireValue(
20
+ manifest.schema.endsWith("/v2") || !image.kind,
21
+ "artifact kinds require family v2",
22
+ );
23
+ const target =
24
+ compose &&
25
+ manifest.images.find(
26
+ (entry) => entry.name === image.targetImage && entry.kind !== "compose",
27
+ );
28
+ requireValue(
29
+ !image.kind || ["image", "compose"].includes(image.kind),
30
+ "unsupported artifact kind",
31
+ );
32
+ requireValue(!compose || target, "Compose target image is absent");
33
+ requireValue(
34
+ image.repository ===
35
+ `ghcr.io/${manifest.repository}/${compose ? target.name : image.name}`,
36
+ "destination must belong to the consumer repository",
37
+ );
38
+ requireValue(
39
+ manifest.images.filter(
40
+ (entry) =>
41
+ entry.repository === image.repository &&
42
+ ociPublicationTag(entry, manifest.version) ===
43
+ ociPublicationTag(image, manifest.version),
44
+ ).length === 1,
45
+ "duplicate publication destination",
46
+ );
47
+ }
48
+
49
+ export function verifyOciGraph({
50
+ image,
51
+ manifest,
52
+ descriptor,
53
+ descriptorFile,
54
+ requireValue: check,
55
+ verifyProvenance,
56
+ }) {
57
+ const manifests = [],
58
+ blobs = new Map(),
59
+ platforms = [],
60
+ attestations = [];
61
+ const visited = new Set();
62
+ const read = (entry) => JSON.parse(fs.readFileSync(descriptorFile(entry)));
63
+ function blob(entry) {
64
+ descriptorFile(entry);
65
+ blobs.set(entry.digest, entry);
66
+ }
67
+ function visit(entry, depth = 0) {
68
+ check(
69
+ depth < 4 && visited.size < 256 && !visited.has(entry.digest),
70
+ "duplicate or excessive OCI graph",
71
+ );
72
+ visited.add(entry.digest);
73
+ const document = read(entry);
74
+ check(
75
+ document.schemaVersion === 2 && document.mediaType === entry.mediaType,
76
+ "OCI descriptor media type mismatch",
77
+ );
78
+ if (indexTypes.includes(document.mediaType)) {
79
+ check(
80
+ image.kind !== "compose" &&
81
+ Array.isArray(document.manifests) &&
82
+ document.manifests.length > 0,
83
+ "invalid OCI index",
84
+ );
85
+ for (const child of document.manifests) visit(child, depth + 1);
86
+ } else {
87
+ check(
88
+ imageTypes.includes(document.mediaType) &&
89
+ Array.isArray(document.layers),
90
+ "unsupported OCI manifest",
91
+ );
92
+ const config = read(document.config);
93
+ blob(document.config);
94
+ for (const layer of document.layers) blob(layer);
95
+ if (image.kind === "compose") {
96
+ verifyCompose(document, image, manifest, read, check);
97
+ } else if (
98
+ entry.annotations?.["vnd.docker.reference.type"] ===
99
+ "attestation-manifest"
100
+ ) {
101
+ check(
102
+ entry.platform?.os === "unknown" &&
103
+ entry.platform?.architecture === "unknown" &&
104
+ document.layers.length > 0 &&
105
+ document.layers.every(
106
+ (layer) => layer.mediaType === "application/vnd.in-toto+json",
107
+ ),
108
+ "invalid image attestation",
109
+ );
110
+ attestations.push({
111
+ target: entry.annotations["vnd.docker.reference.digest"],
112
+ statements: document.layers.map(read),
113
+ });
114
+ } else {
115
+ const platform = `${config.os}/${config.architecture}`;
116
+ check(
117
+ /^linux\/(amd64|arm64)$/u.test(platform) &&
118
+ (!entry.platform ||
119
+ platform ===
120
+ `${entry.platform.os}/${entry.platform.architecture}`),
121
+ "OCI platform mismatch",
122
+ );
123
+ check(
124
+ !platforms.some((found) => found.platform === platform),
125
+ "duplicate image platform",
126
+ );
127
+ platforms.push({ platform, digest: entry.digest });
128
+ verifyProvenance(config);
129
+ }
130
+ }
131
+ manifests.push(entry);
132
+ }
133
+ visit(descriptor);
134
+ if (image.kind !== "compose") {
135
+ const expected =
136
+ image.platform === "multi-platform" ? image.platforms : [image.platform];
137
+ check(
138
+ Array.isArray(expected) &&
139
+ expected.length > 0 &&
140
+ new Set(expected).size === expected.length &&
141
+ JSON.stringify([...expected].sort()) ===
142
+ JSON.stringify(platforms.map((entry) => entry.platform).sort()),
143
+ "incomplete image platform family",
144
+ );
145
+ for (const attestation of attestations) {
146
+ check(
147
+ platforms.some((entry) => entry.digest === attestation.target) &&
148
+ attestation.statements.every((statement) =>
149
+ statement.subject?.some(
150
+ (subject) =>
151
+ `sha256:${subject.digest?.sha256}` === attestation.target,
152
+ ),
153
+ ),
154
+ "image attestation subject mismatch",
155
+ );
156
+ }
157
+ }
158
+ return { manifests, blobs: [...blobs.values()], platforms };
159
+ }
160
+
161
+ function verifyCompose(document, image, manifest, read, check) {
162
+ if (image.preview)
163
+ check(
164
+ image.preview.alias === "compose-preview" &&
165
+ /^\.github\/workflows\/[a-z0-9-]+\.yml$/u.test(
166
+ image.preview.qualificationWorkflow,
167
+ ) &&
168
+ /^(none|sha256:[0-9a-f]{64})$/u.test(image.preview.previousDigest),
169
+ "invalid Compose preview policy",
170
+ );
171
+ check(
172
+ image.platform === "compose" &&
173
+ document.artifactType === composeType &&
174
+ document.config.mediaType === "application/vnd.oci.empty.v1+json" &&
175
+ document.layers.length === 1 &&
176
+ document.layers[0].mediaType ===
177
+ "application/vnd.docker.compose.file+yaml",
178
+ "unsupported Compose artifact",
179
+ );
180
+ // JSON is a YAML subset; this bounded public form needs no executable YAML loader.
181
+ const project = read(document.layers[0]);
182
+ check(
183
+ project.services && Object.keys(project.services).length > 0,
184
+ "empty Compose application",
185
+ );
186
+ const target = manifest.images.find(
187
+ (entry) => entry.name === image.targetImage,
188
+ );
189
+ const references = Object.values(project.services).map(
190
+ (service) => service.image,
191
+ );
192
+ check(
193
+ references.every(
194
+ (ref) =>
195
+ typeof ref === "string" &&
196
+ /^[a-z0-9./:_-]+@sha256:[0-9a-f]{64}$/u.test(ref),
197
+ ) && references.includes(`${target.repository}@${target.digest}`),
198
+ "Compose images must bind exact digests",
199
+ );
200
+ check(
201
+ image.content?.sourceSha === manifest.sourceSha &&
202
+ image.content?.version === manifest.version &&
203
+ image.action === "built",
204
+ "Compose candidate provenance mismatch",
205
+ );
206
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { paperRuntimeSourceMatches } from "./paper-runtime-channels.js";
3
4
 
4
5
  import {
5
6
  PAPER_PATHS,
@@ -12,7 +13,13 @@ import {
12
13
  stableJson,
13
14
  workCheck,
14
15
  } from "./paper-repository.js";
15
- import { mergeNextDevelopmentAgentInstructions } from "./next-development-projection.js";
16
+ import {
17
+ mergeNextDevelopmentAgentInstructions,
18
+ NEXT_DEVELOPMENT_AGENT_SECTION_START,
19
+ NEXT_DEVELOPMENT_AGENT_SECTION_END,
20
+ NEXT_DEVELOPMENT_LOCAL_COMMAND,
21
+ } from "./next-development-projection.js";
22
+ import { NEXT_DEVELOPMENT_ADR } from "./next-development-transition.js";
16
23
 
17
24
  export const PAPER_AGENT_ENTRY_CONTRACT = "kungfu-buildchain-paper-agent-entry";
18
25
  export const PAPER_AGENT_ENTRY_SCHEMA_VERSION = 1;
@@ -148,6 +155,24 @@ export function createPaperAgentEntry({
148
155
  };
149
156
  }
150
157
 
158
+ function mergePaperNextDevelopmentInstructions(current) {
159
+ const merged = mergeNextDevelopmentAgentInstructions(current);
160
+ const start = merged.indexOf(NEXT_DEVELOPMENT_AGENT_SECTION_START);
161
+ const end = merged.indexOf(NEXT_DEVELOPMENT_AGENT_SECTION_END);
162
+ const section = merged.slice(start, end)
163
+ .replace(
164
+ `\`${NEXT_DEVELOPMENT_ADR}\``,
165
+ `[Buildchain next-development ADR](https://github.com/kungfu-systems/buildchain/blob/v4/${NEXT_DEVELOPMENT_ADR})`,
166
+ )
167
+ .replace(
168
+ NEXT_DEVELOPMENT_LOCAL_COMMAND,
169
+ NEXT_DEVELOPMENT_LOCAL_COMMAND.replace(
170
+ "node scripts/", "node node_modules/@kungfu-tech/buildchain/scripts/",
171
+ ),
172
+ );
173
+ return `${merged.slice(0, start)}${section}${merged.slice(end)}`;
174
+ }
175
+
151
176
  export function paperAgentEntryFiles({
152
177
  cwd,
153
178
  buildchainVersion,
@@ -168,7 +193,7 @@ export function paperAgentEntryFiles({
168
193
  [PAPER_PATHS.agentEntry, jsonText(entry)],
169
194
  [
170
195
  PAPER_PATHS.agentInstructions,
171
- mergeNextDevelopmentAgentInstructions(
196
+ mergePaperNextDevelopmentInstructions(
172
197
  mergePaperAgentEntryInstructions(currentAgents, {
173
198
  developmentRef: resolvedDevelopmentRef,
174
199
  }),
@@ -256,6 +281,7 @@ export function collectPaperAgentEntry({
256
281
  buildchainSha = "",
257
282
  mode = "contract",
258
283
  env = process.env,
284
+ runtimeAdmission,
259
285
  } = {}) {
260
286
  const resolvedCwd = path.resolve(cwd);
261
287
  const developmentRef = paperDevelopmentRef(resolvedCwd);
@@ -311,8 +337,8 @@ export function collectPaperAgentEntry({
311
337
  workCheck(
312
338
  "agent-entry.runtime-source",
313
339
  GIT_SHA_PATTERN.test(effectiveBuildchainSha) &&
314
- entry.runtime?.sourceSha === effectiveBuildchainSha,
315
- "The agent-entry contract is bound to the exact executing Buildchain source SHA.",
340
+ paperRuntimeSourceMatches(entry.runtime, effectiveBuildchainSha, mode, runtimeAdmission),
341
+ "The executing Buildchain source is pinned locally or admitted by its v4 channel contract in CI.",
316
342
  "buildchain paper migrate --write --json",
317
343
  ),
318
344
  workCheck(