@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.
- package/AGENTS.md +10 -1
- package/CONTRIBUTING.md +24 -0
- package/README.md +2 -2
- package/actions/promote-buildchain-ref/README.md +1 -1
- package/actions/v4-release-candidate-promote/README.md +28 -0
- package/architecture/agent-change-map.md +70 -9
- package/architecture/ci-lane-change-budget.json +384 -0
- package/architecture/internal-capabilities.json +56 -9
- package/architecture/maintainability-debt.json +162 -107
- package/architecture/maintainability-policy.json +34 -30
- package/architecture/release-tail-contract-inventory.json +6 -6
- package/architecture/v4-architecture-constitution.md +23 -20
- package/architecture/v4-cross-platform-adopter-qualification.json +1 -1
- package/architecture/v4-delivery-authority-parity.json +2 -2
- package/architecture/v4-delivery-warrant-shadow-bootstrap-plan.json +1 -1
- package/architecture/v4-floating-consumer-policy.json +2 -2
- package/architecture/v4-next-development-parity.json +5 -5
- package/architecture/v4-partial-mutation-recovery-qualification.json +4 -1
- package/architecture/v4-platform-stage-checkpoints.json +1 -1
- package/architecture/v4-provider-operation-journal-contract.json +5 -2
- package/architecture/v4-publication-facade-patches.json +64 -0
- package/architecture/v4-release-activation-shadow-domain.json +5 -2
- package/architecture/v4-release-topology.json +198 -38
- package/architecture/v4-rust-wasm-production-authority.json +58 -0
- package/architecture/v4-stable-publication-fence.json +5 -2
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/architecture/v4-tail-reseal-parity.json +8 -6
- package/architecture/v4-universal-workflow-bootstrap.json +176 -0
- package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
- package/architecture/v4-universal-workflow-train-admission.json +40 -0
- package/architecture/workflow-taxonomy.json +1462 -0
- package/bin/buildchain.mjs +7 -7
- package/contracts/buildchain-v2-residuals-v1.json +57 -48
- package/contracts/dev-delivery-authority-v2.schema.json +16 -2
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-canonical-contracts-v1.schema.json +1 -0
- package/dist/site/buildchain-contract.json +93 -48
- package/dist/site/buildchain-site.json +276 -72
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/controller-registry.json +50 -13
- package/dist/site/kfd-claims.json +2610 -983
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +14 -14
- package/dist/site/node-api-registry.json +273 -127
- package/dist/site/page-registry.json +257 -53
- package/dist/site/public-surface-audit.json +3239 -1232
- package/dist/site/publication-authority-registry.json +1135 -363
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -1
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +16 -2
- package/dist/site/site-manifest.json +18 -18
- package/dist/site/workflow-registry.json +5097 -1659
- package/docs/MAP.md +2 -0
- package/docs/auditable-demo.md +1 -1
- package/docs/cli.md +21 -8
- package/docs/dev-delivery-warrant.md +1 -1
- package/docs/engineering-housekeeper.md +3 -3
- package/docs/kfd-support.md +26 -2
- package/docs/next-development-transition.md +2 -2
- package/docs/node-api-reference.md +216 -205
- package/docs/oci-publication.md +160 -0
- package/docs/publication-authority.md +2 -2
- package/docs/publish-transaction.md +36 -3
- package/docs/release-candidate.md +3 -3
- package/docs/release-flow.md +67 -19
- package/docs/release-governance.md +8 -8
- package/docs/release-passport.md +27 -2
- package/docs/reusable-build-surface.md +16 -0
- package/docs/v4-adopter-delivery.md +2 -2
- package/docs/v4-production-release.md +71 -8
- package/docs/v4-rust-wasm-production-authority.md +58 -0
- package/docs/v4-stage-capsule.md +2 -2
- package/docs/v4-tail-reseal.md +1 -1
- package/docs/workflow-catalog.md +155 -0
- package/docs/workflow-path-migration.md +109 -0
- package/package.json +12 -7
- package/packages/core/artifact-verification-envelope.js +5 -5
- package/packages/core/buildchain-contract.js +1 -1
- package/packages/core/buildchain-kfd-claims.js +2 -2
- package/packages/core/buildchain-publication-authority.js +43 -26
- package/packages/core/buildchain-v4-domain.wasm +0 -0
- package/packages/core/ci-lane-change-budget.js +14 -1
- package/packages/core/controller-evidence.js +4 -1
- package/packages/core/dev-delivery-candidate-identity.js +40 -6
- package/packages/core/dev-delivery-warrant-state.js +3 -4
- package/packages/core/dev-delivery-warrant.js +12 -2
- package/packages/core/github-governance-authority.js +4 -4
- package/packages/core/kfd-adopter-manifest.js +46 -47
- package/packages/core/next-development-projection.js +2 -2
- package/packages/core/next-development-transition.js +5 -6
- package/packages/core/oci-compose-qualification.js +140 -0
- package/packages/core/oci-publication-bundle.js +281 -0
- package/packages/core/oci-publication-graph.js +206 -0
- package/packages/core/paper-agent-entry.js +30 -4
- package/packages/core/paper-fleet.js +1 -1
- package/packages/core/paper-runtime-channels.js +348 -0
- package/packages/core/paper-scaffold-content.js +10 -0
- package/packages/core/paper.js +37 -71
- package/packages/core/publication-sealed-bundle.js +78 -13
- package/packages/core/release-line-dry-run.js +1 -1
- package/packages/core/release-passport-contract.js +5 -5
- package/packages/core/release-passport.js +5 -4
- package/packages/core/release-tail-product-capabilities.js +38 -0
- package/packages/core/release-tail-provider-plane.js +68 -1048
- package/packages/core/v4-canonical-contracts.js +15 -75
- package/packages/core/v4-domain-wasm-artifact.js +7 -0
- package/packages/core/v4-domain-wasm.js +216 -0
- package/packages/core/v4-floating-consumer-evidence.js +1 -1
- package/packages/core/v4-floating-consumer-policy.js +1 -1
- package/packages/core/v4-partial-mutation-recovery-qualification.js +9 -386
- package/packages/core/v4-product-publication.js +59 -0
- package/packages/core/v4-protected-publication-source.js +19 -21
- package/packages/core/v4-provider-operation-journal.js +18 -487
- package/packages/core/v4-provider-readback-idempotency.js +12 -97
- package/packages/core/v4-release-activation-shadow.js +13 -500
- package/packages/core/v4-release-invocation.js +28 -389
- package/packages/core/v4-stable-publication-fence.js +13 -342
- package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
- package/scripts/binary-publication-evidence.mjs +135 -0
- package/scripts/build-contract-core.mjs +1 -14
- package/scripts/build-v4-domain-wasm.mjs +189 -0
- package/scripts/check-action-bundles.mjs +12 -2
- package/scripts/check-ci-lane-change-budget.mjs +4 -1
- package/scripts/check-core-mechanism-inventory.mjs +4 -3
- package/scripts/check-inventory.mjs +35 -44
- package/scripts/check-maintainability.mjs +16 -113
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +2 -2
- package/scripts/check-v4-public-dogfood-contract.mjs +22 -9
- package/scripts/check-v4-release-topology.mjs +236 -50
- package/scripts/check-v4-runtime-semantic-closure.mjs +4 -1
- package/scripts/check-v4-universal-workflow-bootstrap.mjs +266 -0
- package/scripts/check-workflow-taxonomy.mjs +6 -0
- package/scripts/copy-v4-domain-wasm.mjs +14 -0
- package/scripts/dev-delivery-authority-command-adapters.mjs +1 -1
- package/scripts/dev-delivery-authority.mjs +8 -8
- package/scripts/dev-delivery-source-proof-reuse.mjs +2 -1
- package/scripts/dev-delivery-warrant-options.mjs +14 -9
- package/scripts/dev-delivery-warrant-store.mjs +73 -7
- package/scripts/dev-delivery-warrant-transition.mjs +109 -0
- package/scripts/dev-delivery-warrant.mjs +76 -99
- package/scripts/dev-pr-auto-merge.mjs +15 -15
- package/scripts/generate-channel-build-workflow.mjs +7 -2
- package/scripts/generate-channel-promotion-workflow.mjs +10 -25
- package/scripts/generate-site-bundle.mjs +11 -11
- package/scripts/generate-v4-universal-workflow-facades.mjs +497 -0
- package/scripts/generate-workflow-taxonomy.mjs +25 -0
- package/scripts/github-output.mjs +31 -0
- package/scripts/maintainability-metrics.mjs +16 -5
- package/scripts/maintainability-public-surface.mjs +115 -0
- package/scripts/next-development-review.mjs +307 -0
- package/scripts/oci-compose-preview.mjs +250 -0
- package/scripts/paper-work-fleet-cli.mjs +2 -0
- package/scripts/publication-candidate-kind.mjs +93 -0
- package/scripts/publication-candidate-sealer.mjs +16 -1
- package/scripts/release-asset-client.mjs +91 -0
- package/scripts/release-candidate-resolver.mjs +23 -35
- package/scripts/resume-from-candidate-run.mjs +11 -18
- package/scripts/site-capability-metadata.mjs +1 -0
- package/scripts/source-verification-evidence.mjs +344 -0
- package/scripts/stable-candidate-qualification.mjs +1 -1
- package/scripts/universal-facade-maintainability.mjs +91 -0
- package/scripts/v4-product-publication-intent.mjs +129 -0
- package/scripts/v4-publication-settlement.mjs +211 -0
- package/scripts/v4-universal-workflow-backflow.mjs +212 -0
- package/scripts/v4-universal-workflow-engine.mjs +620 -0
- package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
- package/scripts/verify-version-state-delta.mjs +173 -0
- package/scripts/workflow-taxonomy.mjs +507 -0
- package/templates/native-dev-delivery.yml +4 -7
- package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
- 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 {
|
|
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
|
-
|
|
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
|
|
315
|
-
"The
|
|
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(
|