@kungfu-tech/buildchain 4.0.2 → 4.0.3-alpha.0
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 +4 -4
- package/CONTRIBUTING.md +34 -0
- package/README.md +1 -1
- package/architecture/agent-change-map.md +154 -147
- package/architecture/{v4-architecture-constitution.md → architecture-constitution.md} +11 -11
- package/architecture/{v3-core-mechanism-inventory.md → baseline-core-mechanism-inventory.md} +1 -1
- package/architecture/{v4-bootstrap-authority.json → bootstrap-authority.json} +3 -3
- package/architecture/{v3-v4-capability-closure.md → capability-parity-closure.md} +3 -3
- package/architecture/{v3-v4-live-capability-inventory.json → capability-parity-live-capability-inventory.json} +4 -4
- package/architecture/{v4-capability-state-machine-manifest.json → capability-state-machine-manifest.json} +2 -2
- package/architecture/{v4-capability-state-machine-manifest.schema.json → capability-state-machine-manifest.schema.json} +2 -2
- package/architecture/ci-lane-change-budget.json +18 -0
- package/architecture/{v4-cross-platform-adopter-qualification.json → cross-platform-adopter-qualification.json} +3 -3
- package/architecture/{v4-delivery-authority-parity.json → delivery-authority-parity.json} +16 -16
- package/architecture/{v4-delivery-warrant-shadow-bootstrap-plan.json → delivery-warrant-shadow-bootstrap-plan.json} +3 -3
- package/architecture/{v4-delivery-warrant-shadow-bootstrap-plan.md → delivery-warrant-shadow-bootstrap-plan.md} +7 -7
- package/architecture/{v4-floating-consumer-policy.md → floating-consumer-policy.md} +1 -1
- package/architecture/implementation-naming.json +1273 -0
- package/architecture/internal-capabilities.json +171 -169
- package/architecture/maintainability-debt.json +87 -82
- package/architecture/maintainability-policy.json +15 -15
- package/architecture/{v4-next-development-parity.json → next-development-parity.json} +8 -8
- package/architecture/{v4-partial-mutation-recovery-qualification.json → partial-mutation-recovery-qualification.json} +8 -8
- package/architecture/{v4-platform-stage-checkpoints.json → platform-stage-checkpoints.json} +2 -2
- package/architecture/{v4-provider-operation-journal-contract.json → provider-operation-journal-contract.json} +9 -9
- package/architecture/{v4-publication-rehearsal-parity.json → publication-rehearsal-parity.json} +4 -4
- package/architecture/{v4-release-activation-shadow-domain.json → release-activation-shadow-domain.json} +8 -8
- package/architecture/{v4-release-topology.json → release-topology.json} +112 -56
- package/architecture/{v4-release-train-parity.json → release-train-parity.json} +2 -2
- package/architecture/{v4-runtime-ref-resume-authority.json → runtime-ref-resume-authority.json} +1 -1
- package/architecture/{v4-runtime-semantic-closure.json → runtime-semantic-closure.json} +2 -2
- package/architecture/{v4-rust-libnode-bridge-evaluation.json → rust-libnode-bridge-evaluation.json} +3 -3
- package/architecture/{v4-rust-libnode-bridge-spike.md → rust-libnode-bridge-spike.md} +6 -6
- package/architecture/{v4-rust-wasm-production-authority.json → rust-wasm-production-authority.json} +11 -11
- package/architecture/{v4-stable-publication-fence.json → stable-publication-fence.json} +6 -6
- package/architecture/{v4-stage-capsule-contract.json → stage-capsule-contract.json} +1 -1
- package/architecture/{v4-stage-capsule-qualification.json → stage-capsule-qualification.json} +6 -6
- package/architecture/{v4-stage-capsule-resume-planner.json → stage-capsule-resume-planner.json} +2 -2
- package/architecture/{v4-stage-capsule-store-contract.json → stage-capsule-store-contract.json} +4 -4
- package/architecture/{v4-tail-reseal-parity.json → tail-reseal-parity.json} +28 -28
- package/architecture/{v4-universal-workflow-bootstrap.json → universal-workflow-bootstrap.json} +9 -4
- package/architecture/{v4-universal-workflow-fault-campaign.json → universal-workflow-fault-campaign.json} +8 -8
- package/architecture/{v4-universal-workflow-train-admission.json → universal-workflow-train-admission.json} +8 -8
- package/architecture/workflow-taxonomy.json +11 -0
- package/bin/buildchain.mjs +3 -3
- package/bin/internal/adopter-delivery-cli.mjs +9 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/crates/{buildchain-v4-bridge → buildchain-host-bridge}/Cargo.lock +1 -1
- package/crates/{buildchain-v4-bridge → buildchain-host-bridge}/Cargo.toml +1 -1
- package/crates/{buildchain-v4-bridge → buildchain-host-bridge}/src/main.rs +3 -3
- package/dist/site/buildchain-contract.json +18 -18
- package/dist/site/buildchain-site.json +167 -71
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/kfd-claims.json +226 -90
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +12929 -10491
- package/dist/site/page-registry.json +155 -59
- package/dist/site/public-surface-audit.json +65 -46
- package/dist/site/publication-authority-registry.json +27 -17
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +18 -9
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +86 -52
- package/docs/MAP.md +4 -3
- package/docs/next-development-transition.md +24 -1
- package/docs/node-api-reference.md +435 -175
- package/docs/oci-publication.md +173 -0
- package/docs/publish-transaction.md +36 -3
- package/docs/release-flow.md +1 -1
- package/docs/reusable-build-surface.md +1 -1
- package/docs/v4-adopter-delivery.md +2 -2
- package/docs/v4-canonical-contracts.md +7 -7
- package/docs/v4-delivery-warrant-read-candidate.md +3 -3
- package/docs/v4-delivery-warrant-semantic-diff.md +3 -3
- package/docs/v4-production-release.md +8 -2
- package/docs/v4-publication-rehearsal.md +3 -3
- package/docs/v4-runtime-ref-resume-authority.md +2 -2
- package/docs/v4-rust-wasm-production-authority.md +6 -6
- package/docs/v4-stage-capsule.md +7 -7
- package/docs/v4-tail-reseal.md +1 -1
- package/docs/workflow-catalog.md +1 -0
- package/package.json +37 -26
- package/packages/compatibility/adopter-delivery.js +16 -0
- package/packages/compatibility/cross-platform-adopter-qualification.js +12 -0
- package/packages/compatibility/floating-consumer-policy.js +14 -0
- package/packages/compatibility/publication-qualification.js +11 -0
- package/packages/compatibility/publication-rehearsal.js +18 -0
- package/packages/compatibility/runtime-ref-resume-authority.js +13 -0
- package/packages/compatibility/tail-reseal-receipt.js +7 -0
- package/packages/compatibility/tail-reseal.js +11 -0
- package/packages/core/{v4-adopter-delivery-parity.js → adopter-delivery-parity.js} +12 -12
- package/packages/core/{v4-adopter-delivery.js → adopter-delivery.js} +24 -24
- package/packages/core/buildchain-contract.js +5 -5
- package/packages/core/buildchain-domain.wasm +0 -0
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/packages/core/{v4-canonical-contracts.js → canonical-contracts.js} +32 -31
- package/packages/core/{v4-cross-platform-adopter-qualification.js → cross-platform-adopter-qualification.js} +19 -19
- package/packages/core/{v4-delivery-warrant-fixture-runner.js → delivery-warrant-fixture-runner.js} +36 -32
- package/packages/core/{v4-delivery-warrant-read-candidate.js → delivery-warrant-read-candidate.js} +40 -40
- package/packages/core/{v4-delivery-warrant-semantic-diff-gate.js → delivery-warrant-semantic-diff-gate.js} +58 -59
- package/packages/core/{v4-delivery-warrant-shadow-adapter.js → delivery-warrant-shadow-adapter.js} +15 -15
- package/packages/core/dev-delivery-execution-transfer.js +6 -6
- package/packages/core/dev-delivery-native-proof.js +12 -8
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +1 -1
- package/packages/core/dev-delivery-warrant-native-compatibility.js +4 -4
- package/packages/core/dev-delivery-warrant-state.js +11 -11
- package/packages/core/dev-delivery-warrant.js +3 -3
- package/packages/core/domain-wasm-artifact.js +7 -0
- package/packages/core/{v4-domain-wasm.js → domain-wasm.js} +31 -31
- package/packages/core/{v4-floating-consumer-evidence.js → floating-consumer-evidence.js} +13 -13
- package/packages/core/{v4-floating-consumer-policy.js → floating-consumer-policy.js} +27 -29
- package/packages/core/{v4-floating-consumer-release-passport.js → floating-consumer-release-passport.js} +25 -25
- package/packages/core/index.js +4 -8
- package/packages/core/next-development-projection.js +24 -1
- package/packages/core/next-development-transition.js +5 -6
- package/packages/core/oci-compose-qualification.js +143 -0
- package/packages/core/oci-publication-bundle.js +281 -0
- package/packages/core/oci-publication-graph.js +206 -0
- package/packages/core/paper-runtime-channels.js +2 -2
- package/packages/core/paper.js +3 -3
- package/packages/core/partial-mutation-recovery-qualification.js +18 -0
- package/packages/core/{v4-platform-stage-checkpoints.js → platform-stage-checkpoints.js} +37 -37
- package/packages/core/{v4-product-publication.js → product-publication.js} +11 -9
- package/packages/core/{v4-protected-publication-source.js → protected-publication-source.js} +3 -3
- package/packages/core/provider-operation-journal.js +62 -0
- package/packages/core/{v4-provider-readback-idempotency.js → provider-readback-idempotency.js} +33 -33
- package/packages/core/public-export-compatibility.js +33 -0
- package/packages/core/publication-development.js +72 -0
- package/packages/core/{v4-publication-qualification.js → publication-qualification.js} +17 -17
- package/packages/core/{v4-publication-rehearsal-capsule.js → publication-rehearsal-capsule.js} +23 -23
- package/packages/core/publication-rehearsal-projection.js +2 -2
- package/packages/core/{v4-publication-rehearsal-provider-bindings.js → publication-rehearsal-provider-bindings.js} +4 -4
- package/packages/core/publication-rehearsal-runtime.js +21 -21
- package/packages/core/{v4-publication-rehearsal.js → publication-rehearsal.js} +33 -33
- package/packages/core/publication-sealed-bundle.js +78 -13
- package/packages/core/release-activation-shadow.js +32 -0
- package/packages/core/release-candidate.js +6 -6
- package/packages/core/{v4-release-invocation.js → release-invocation.js} +15 -15
- package/packages/core/release-passport.js +36 -36
- package/packages/core/release-tail-product-capabilities.js +9 -0
- package/packages/core/release-tail-provider-plane.js +10 -10
- package/packages/core/{v4-runtime-ref-resume-authority.js → runtime-ref-resume-authority.js} +36 -40
- package/packages/core/{v4-runtime-selector-persistence.js → runtime-selector-persistence.js} +7 -7
- package/packages/core/stable-publication-fence.js +28 -0
- package/packages/core/{v4-stage-capsule-local-store.js → stage-capsule-local-store.js} +56 -54
- package/packages/core/{v4-stage-capsule-qualification-campaign.js → stage-capsule-qualification-campaign.js} +37 -38
- package/packages/core/{v4-stage-capsule-qualification.js → stage-capsule-qualification.js} +28 -29
- package/packages/core/{v4-stage-capsule-resume-planner.js → stage-capsule-resume-planner.js} +29 -29
- package/packages/core/{v4-stage-capsule-store.js → stage-capsule-store.js} +59 -56
- package/packages/core/{v4-stage-capsule.js → stage-capsule.js} +45 -45
- package/packages/core/{v4-tail-reseal-contract.js → tail-reseal-contract.js} +6 -6
- package/packages/core/{v4-tail-reseal-github.js → tail-reseal-github.js} +12 -12
- package/packages/core/{v4-tail-reseal-receipt.js → tail-reseal-receipt.js} +22 -22
- package/packages/core/{v4-tail-reseal.js → tail-reseal.js} +29 -29
- package/packages/core/{v4-universal-workflow-bootstrap.js → universal-workflow-bootstrap.js} +23 -23
- package/scripts/{v4-architecture.mjs → architecture.mjs} +6 -6
- package/scripts/binary-publication-evidence.mjs +1 -1
- package/scripts/{v4-bridge-bootstrap.mjs → bridge-bootstrap.mjs} +14 -18
- package/scripts/{v4-bridge-evidence.mjs → bridge-evidence.mjs} +8 -8
- package/scripts/{build-v4-domain-wasm.mjs → build-domain-wasm.mjs} +6 -18
- package/scripts/{v3-v4-capability-catalog.mjs → capability-parity-catalog.mjs} +19 -19
- package/scripts/check-action-bundles.mjs +2 -2
- package/scripts/{check-v3-v4-capability-inventory.mjs → check-capability-parity-inventory.mjs} +42 -42
- package/scripts/check-core-mechanism-inventory.mjs +4 -1
- package/scripts/{check-v4-floating-consumer-policy-contract.mjs → check-floating-consumer-policy-contract.mjs} +8 -8
- package/scripts/check-implementation-naming.mjs +135 -0
- package/scripts/check-inventory.mjs +6 -5
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/{check-v4-public-dogfood-contract.mjs → check-public-dogfood-contract.mjs} +15 -15
- package/scripts/{check-v4-release-topology.mjs → check-release-topology.mjs} +53 -31
- package/scripts/{check-v4-runtime-semantic-closure.mjs → check-runtime-semantic-closure.mjs} +19 -16
- package/scripts/{check-v4-universal-workflow-bootstrap.mjs → check-universal-workflow-bootstrap.mjs} +14 -14
- package/scripts/{v4-consumer-policy.mjs → consumer-policy.mjs} +15 -15
- package/scripts/{copy-v4-domain-wasm.mjs → copy-domain-wasm.mjs} +3 -3
- package/scripts/{v4-cross-platform-adopter-qualification.mjs → cross-platform-adopter-qualification.mjs} +9 -9
- package/scripts/{v4-declarative-promotion-admission.mjs → declarative-promotion-admission.mjs} +4 -4
- package/scripts/dev-delivery-warrant-options.mjs +1 -1
- package/scripts/dev-delivery-warrant-store.mjs +29 -10
- package/scripts/dev-delivery-warrant.mjs +4 -4
- package/scripts/dev-pr-delivery-warrant.mjs +1 -1
- package/scripts/generate-channel-build-workflow.mjs +2 -2
- package/scripts/generate-channel-promotion-workflow.mjs +14 -31
- package/scripts/generate-release-candidate-passport.mjs +23 -23
- package/scripts/generate-site-bundle.mjs +17 -17
- package/scripts/{generate-v4-universal-workflow-facades.mjs → generate-universal-workflow-facades.mjs} +61 -21
- package/scripts/{v4-host-adapter.mjs → host-adapter.mjs} +1 -1
- package/scripts/implementation-references.mjs +25 -0
- package/scripts/init-repo.mjs +1 -1
- package/scripts/next-development-review.mjs +17 -11
- package/scripts/oci-compose-preview.mjs +261 -0
- package/scripts/{v4-platform-stage-checkpoint-rehearsal.mjs → platform-stage-checkpoint-rehearsal.mjs} +17 -17
- package/scripts/{v4-product-publication-intent.mjs → product-publication-intent.mjs} +16 -8
- package/scripts/publication-candidate-kind.mjs +93 -0
- package/scripts/publication-candidate-sealer.mjs +16 -1
- package/scripts/{v4-publication-rehearsal-fixture.mjs → publication-rehearsal-fixture.mjs} +5 -5
- package/scripts/{v4-publication-settlement.mjs → publication-settlement.mjs} +8 -8
- package/scripts/{v4-release-candidate-adapter.mjs → release-candidate-adapter.mjs} +4 -4
- package/scripts/release-candidate-resolver.mjs +15 -26
- package/scripts/release-tail.mjs +5 -5
- package/scripts/resume-from-candidate-run.mjs +35 -42
- package/scripts/site-capability-metadata.mjs +9 -0
- package/scripts/source-verification-evidence.mjs +7 -7
- package/scripts/{v4-stage-capsule-qualification.mjs → stage-capsule-qualification.mjs} +62 -62
- package/scripts/{v4-stage-capsule-resume-rehearsal.mjs → stage-capsule-resume-rehearsal.mjs} +6 -6
- package/scripts/{v4-tail-reseal-macos-rehearsal.mjs → tail-reseal-macos-rehearsal.mjs} +9 -9
- package/scripts/{v4-tail-reseal.mjs → tail-reseal.mjs} +19 -19
- package/scripts/universal-facade-maintainability.mjs +12 -3
- package/scripts/{v4-universal-workflow-backflow.mjs → universal-workflow-backflow.mjs} +10 -10
- package/scripts/{v4-universal-workflow-engine.mjs → universal-workflow-engine.mjs} +40 -38
- package/scripts/{v4-universal-workflow-self-dogfood.mjs → universal-workflow-self-dogfood.mjs} +12 -12
- package/scripts/verify-version-state-delta.mjs +4 -2
- package/scripts/{v4-warrant-shadow-plan.mjs → warrant-shadow-plan.mjs} +6 -6
- package/templates/universal-buildchain-bootstrap-recovery.yml +4 -4
- package/packages/core/buildchain-v4-domain.wasm +0 -0
- package/packages/core/v4-domain-wasm-artifact.js +0 -7
- package/packages/core/v4-partial-mutation-recovery-qualification.js +0 -18
- package/packages/core/v4-provider-operation-journal.js +0 -62
- package/packages/core/v4-release-activation-shadow.js +0 -32
- package/packages/core/v4-stable-publication-fence.js +0 -28
- /package/actions/{v4-release-candidate-promote → release-candidate-promote}/README.md +0 -0
- /package/architecture/{v4-adopter-delivery-parity.json → adopter-delivery-parity.json} +0 -0
- /package/architecture/{v3-core-mechanism-inventory.json → baseline-core-mechanism-inventory.json} +0 -0
- /package/architecture/{v4-canonical-contract-fixtures.json → canonical-contract-fixtures.json} +0 -0
- /package/architecture/{v4-compatibility-facts-parity.json → compatibility-facts-parity.json} +0 -0
- /package/architecture/{v4-delivery-warrant-shadow-bootstrap-plan.schema.json → delivery-warrant-shadow-bootstrap-plan.schema.json} +0 -0
- /package/architecture/{v4-delivery-warrant-shadow-fixtures.json → delivery-warrant-shadow-fixtures.json} +0 -0
- /package/architecture/{v4-delivery-warrant-shadow-fixtures.schema.json → delivery-warrant-shadow-fixtures.schema.json} +0 -0
- /package/architecture/{v4-exception-ledger.json → exception-ledger.json} +0 -0
- /package/architecture/{v4-exception-ledger.schema.json → exception-ledger.schema.json} +0 -0
- /package/architecture/{v4-floating-consumer-policy.json → floating-consumer-policy.json} +0 -0
- /package/architecture/{v4-publication-facade-patches.json → publication-facade-patches.json} +0 -0
- /package/architecture/{v4-release-invocation-fixtures.json → release-invocation-fixtures.json} +0 -0
|
@@ -175,7 +175,13 @@ confidence: high
|
|
|
175
175
|
sensitivity: public
|
|
176
176
|
evidence_grade: A
|
|
177
177
|
review_state: self-reviewed
|
|
178
|
-
last_reviewed: 2026-
|
|
178
|
+
last_reviewed: 2026-09-07
|
|
179
|
+
ai_provenance:
|
|
180
|
+
model_family: GPT-6
|
|
181
|
+
product: Codex
|
|
182
|
+
generated_at: 2026-09-07
|
|
183
|
+
visible_context: Release completion, protected development provider, and independent review sources.
|
|
184
|
+
invisible_context_boundary: No private credentials or external unpublished release state.
|
|
179
185
|
---
|
|
180
186
|
|
|
181
187
|
# Next-development Transition
|
|
@@ -229,6 +235,23 @@ can settle, Patrol reads every prepared version path at the candidate SHA and
|
|
|
229
235
|
requires the exact reserved blob identities; missing or stale state blocks
|
|
230
236
|
before a Release Cut or heavy candidate build.
|
|
231
237
|
|
|
238
|
+
## Stable publication and the next patch
|
|
239
|
+
|
|
240
|
+
A completed stable publication with \`semver/auto\` schedules the next patch at
|
|
241
|
+
Alpha zero: \`4.0.2\` prepares \`4.0.3-alpha.0\`. Publication settlement is
|
|
242
|
+
retained before the development PR starts. Preparation reads the exact current
|
|
243
|
+
protected Dev commit and regenerates only its declared version files, preserving
|
|
244
|
+
code that arrived after the stable candidate. The independent reviewer verifies
|
|
245
|
+
the completed stable publication and reconstructs every tracked byte from the
|
|
246
|
+
protected parent; a PR body alone grants no authority.
|
|
247
|
+
|
|
248
|
+
A replay returns \`already-current\` or \`already-advanced\` when Dev already
|
|
249
|
+
contains the target or a later version. A late Alpha completion cannot regress
|
|
250
|
+
Dev to an older patch. A moved protected base fails before creating the version
|
|
251
|
+
commit and requires a fresh preparation attempt. Pending review leaves the
|
|
252
|
+
completed publication intact. Anchored version models still require a reviewed
|
|
253
|
+
manual anchor.
|
|
254
|
+
|
|
232
255
|
## Version models
|
|
233
256
|
|
|
234
257
|
\`semver/auto\` increments the Alpha sequence on the same semantic patch. For
|
|
@@ -159,15 +159,14 @@ function normalizeModel(value = {}) {
|
|
|
159
159
|
return model;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function normalizeCompletedAlpha(value = {}) {
|
|
162
|
+
function normalizeCompletedAlpha(value = {}, model) {
|
|
163
163
|
const version = String(value.version || "").trim();
|
|
164
|
-
|
|
164
|
+
const expression = model?.strategy === "anchored" ? /-alpha\.(0|[1-9]\d*)$/u : ALPHA;
|
|
165
|
+
if (!SEMVER.test(version) || !expression.test(version))
|
|
165
166
|
throw new Error("completedAlpha.version must be an alpha semantic version");
|
|
166
167
|
const exactTag = String(value.exactTag || "").trim();
|
|
167
168
|
if (exactTag !== `v${version}`)
|
|
168
|
-
throw new Error(
|
|
169
|
-
"completedAlpha.exactTag must match completedAlpha.version",
|
|
170
|
-
);
|
|
169
|
+
throw new Error("completedAlpha.exactTag must match completedAlpha.version");
|
|
171
170
|
const normalized = {
|
|
172
171
|
outcome: String(value.outcome || "").trim(),
|
|
173
172
|
version,
|
|
@@ -251,8 +250,8 @@ function initialStateRoot(status, idempotencyKey) {
|
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
export function createNextDevelopmentTransition(input = {}) {
|
|
254
|
-
const completedAlpha = normalizeCompletedAlpha(input.completedAlpha);
|
|
255
253
|
const model = normalizeModel(input.model);
|
|
254
|
+
const completedAlpha = normalizeCompletedAlpha(input.completedAlpha, model);
|
|
256
255
|
const adapter = normalizeAdapter(input);
|
|
257
256
|
const declaredPaths = [
|
|
258
257
|
...adapter.sourcePaths,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { domainContentRoot } from "./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 === domainContentRoot("oci-publication-family", body) &&
|
|
24
|
+
family.repository === repository &&
|
|
25
|
+
family.version === version &&
|
|
26
|
+
family.sourceSha === documents.passport.source.builtSourceSha &&
|
|
27
|
+
documents.passport.source.builtSourceTreeSha ===
|
|
28
|
+
documents.invocation.candidate.tree &&
|
|
29
|
+
readback.sourceSha === documents.invocation.candidate.commit &&
|
|
30
|
+
readback.familyRoot === root &&
|
|
31
|
+
readback.version === version &&
|
|
32
|
+
readback.candidateSourceSha === family.sourceSha &&
|
|
33
|
+
operation?.receipt.evidenceRoots.includes(releaseTailRoot(readback)),
|
|
34
|
+
"unbound public OCI family",
|
|
35
|
+
);
|
|
36
|
+
const applications = family.images.filter(
|
|
37
|
+
(entry) => entry.kind === "compose" && entry.preview,
|
|
38
|
+
);
|
|
39
|
+
check(applications.length === 1, "expected one declared Compose preview");
|
|
40
|
+
const application = applications[0],
|
|
41
|
+
policy = application.preview;
|
|
42
|
+
check(
|
|
43
|
+
policy.alias === "compose-preview" &&
|
|
44
|
+
/^\.github\/workflows\/[a-z0-9-]+\.yml$/u.test(
|
|
45
|
+
policy.qualificationWorkflow,
|
|
46
|
+
) &&
|
|
47
|
+
/^(none|sha256:[0-9a-f]{64})$/u.test(policy.previousDigest),
|
|
48
|
+
"invalid preview policy",
|
|
49
|
+
);
|
|
50
|
+
const image = family.images.find(
|
|
51
|
+
(entry) =>
|
|
52
|
+
entry.name === application.targetImage && entry.kind !== "compose",
|
|
53
|
+
);
|
|
54
|
+
check(
|
|
55
|
+
image &&
|
|
56
|
+
application.repository === image.repository &&
|
|
57
|
+
image.repository === `ghcr.io/${repository}/${image.name}` &&
|
|
58
|
+
/^[a-z0-9][a-z0-9._-]*$/u.test(image.name),
|
|
59
|
+
"foreign application destination",
|
|
60
|
+
);
|
|
61
|
+
for (const entry of [image, application])
|
|
62
|
+
check(
|
|
63
|
+
readback.images.some(
|
|
64
|
+
(observed) =>
|
|
65
|
+
observed.name === entry.name &&
|
|
66
|
+
observed.repository === entry.repository &&
|
|
67
|
+
observed.digest === entry.digest &&
|
|
68
|
+
observed.ref === ociPublicationTag(entry, version) &&
|
|
69
|
+
observed.anonymous === true,
|
|
70
|
+
),
|
|
71
|
+
"missing public artifact readback",
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
application,
|
|
75
|
+
image,
|
|
76
|
+
policy,
|
|
77
|
+
familyRoot: root,
|
|
78
|
+
repository,
|
|
79
|
+
tag,
|
|
80
|
+
sourceSha: documents.product.publication.releaseSha,
|
|
81
|
+
publicationReceiptRoot: documents.receipt.receiptRoot,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function verifyComposeQualification(context, run, receipt) {
|
|
86
|
+
const { application, image, policy, repository, tag, sourceSha, familyRoot } =
|
|
87
|
+
context;
|
|
88
|
+
check(
|
|
89
|
+
run.repository?.full_name === repository &&
|
|
90
|
+
run.head_repository?.full_name === repository &&
|
|
91
|
+
run.event === "workflow_dispatch" &&
|
|
92
|
+
run.status === "completed" &&
|
|
93
|
+
run.conclusion === "success" &&
|
|
94
|
+
run.head_sha === sourceSha &&
|
|
95
|
+
run.path.split("@")[0] === policy.qualificationWorkflow,
|
|
96
|
+
"qualification run is not the exact published source",
|
|
97
|
+
);
|
|
98
|
+
if (!receipt) return;
|
|
99
|
+
check(
|
|
100
|
+
receipt.schema === "kungfu-buildchain-compose-qualification/v1" &&
|
|
101
|
+
receipt.repository === repository &&
|
|
102
|
+
receipt.tag === tag &&
|
|
103
|
+
receipt.sourceSha === sourceSha &&
|
|
104
|
+
receipt.familyRoot === familyRoot &&
|
|
105
|
+
String(receipt.runId) === String(run.id) &&
|
|
106
|
+
Number(receipt.runAttempt) === run.run_attempt &&
|
|
107
|
+
receipt.image === `${image.repository}@${image.digest}` &&
|
|
108
|
+
receipt.application ===
|
|
109
|
+
`${application.repository}@${application.digest}` &&
|
|
110
|
+
receipt.previousDigest === policy.previousDigest &&
|
|
111
|
+
receipt.passed === true,
|
|
112
|
+
"qualification receipt identity mismatch",
|
|
113
|
+
);
|
|
114
|
+
for (const name of [
|
|
115
|
+
"freshInstall",
|
|
116
|
+
"restartPersistence",
|
|
117
|
+
"upgradePersistence",
|
|
118
|
+
"rollbackPersistence",
|
|
119
|
+
"accountIsolation",
|
|
120
|
+
"hardenedRuntime",
|
|
121
|
+
])
|
|
122
|
+
check(receipt.checks?.[name] === true, `qualification lacks ${name}`);
|
|
123
|
+
check(
|
|
124
|
+
Array.isArray(image.platforms) &&
|
|
125
|
+
image.platforms.length === 2 &&
|
|
126
|
+
JSON.stringify([...image.platforms].sort()) ===
|
|
127
|
+
JSON.stringify(["linux/amd64", "linux/arm64"]) &&
|
|
128
|
+
image.platforms.every(
|
|
129
|
+
(platform) => receipt.platforms?.[platform]?.passed === true,
|
|
130
|
+
),
|
|
131
|
+
"qualification lacks both platforms",
|
|
132
|
+
);
|
|
133
|
+
check(
|
|
134
|
+
Array.isArray(receipt.evidence) &&
|
|
135
|
+
receipt.evidence.length > 0 &&
|
|
136
|
+
receipt.evidence.every(
|
|
137
|
+
(entry) =>
|
|
138
|
+
/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.json$/u.test(entry.path) &&
|
|
139
|
+
/^sha256:[0-9a-f]{64}$/u.test(entry.sha256),
|
|
140
|
+
),
|
|
141
|
+
"missing qualification evidence bindings",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { domainContentRoot } from "./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: domainContentRoot("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 = domainContentRoot("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
|
+
}
|