@kungfu-tech/buildchain 4.0.2-alpha.44 → 4.0.2-alpha.45

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 (37) hide show
  1. package/architecture/maintainability-debt.json +18 -19
  2. package/architecture/maintainability-policy.json +4 -4
  3. package/architecture/v4-release-topology.json +9 -6
  4. package/architecture/v4-universal-workflow-train-admission.json +1 -1
  5. package/dist/site/buildchain-contract.json +7 -7
  6. package/dist/site/buildchain-site.json +47 -11
  7. package/dist/site/capability-registry.json +2 -2
  8. package/dist/site/kfd-claims.json +29 -5
  9. package/dist/site/kfd-upstream-aggregate.json +1 -1
  10. package/dist/site/manual-registry.json +2 -2
  11. package/dist/site/node-api-registry.json +142 -1
  12. package/dist/site/page-registry.json +41 -5
  13. package/dist/site/public-surface-audit.json +7 -3
  14. package/dist/site/publication-authority-registry.json +5 -17
  15. package/dist/site/publication-registry.json +4 -4
  16. package/dist/site/release-provenance.json +2 -1
  17. package/dist/site/site-manifest.json +6 -6
  18. package/dist/site/workflow-registry.json +5 -5
  19. package/docs/MAP.md +1 -0
  20. package/docs/node-api-reference.md +11 -0
  21. package/docs/oci-publication.md +82 -0
  22. package/package.json +3 -2
  23. package/packages/core/buildchain-v4-domain.wasm +0 -0
  24. package/packages/core/oci-publication-bundle.js +241 -0
  25. package/packages/core/release-tail-product-capabilities.js +9 -0
  26. package/packages/core/v4-canonical-contracts.js +1 -0
  27. package/packages/core/v4-domain-wasm-artifact.js +2 -2
  28. package/scripts/check-v4-release-topology.mjs +14 -4
  29. package/scripts/generate-channel-promotion-workflow.mjs +2 -19
  30. package/scripts/generate-v4-universal-workflow-facades.mjs +8 -2
  31. package/scripts/publication-candidate-kind.mjs +92 -0
  32. package/scripts/publication-candidate-sealer.mjs +9 -0
  33. package/scripts/release-candidate-resolver.mjs +6 -17
  34. package/scripts/resume-from-candidate-run.mjs +10 -17
  35. package/scripts/site-capability-metadata.mjs +1 -0
  36. package/scripts/v4-product-publication-intent.mjs +1 -1
  37. package/scripts/v4-universal-workflow-engine.mjs +2 -0
@@ -6,7 +6,7 @@
6
6
  "cliCommandCount": 117,
7
7
  "workflowCount": 114,
8
8
  "actionCount": 8,
9
- "sitePageCount": 87,
9
+ "sitePageCount": 88,
10
10
  "docCommandRefCount": 1050,
11
11
  "failureCount": 0
12
12
  },
@@ -5518,6 +5518,10 @@
5518
5518
  "id": "manual:observed-evidence-patrol",
5519
5519
  "category": "manual"
5520
5520
  },
5521
+ {
5522
+ "id": "manual:oci-publication",
5523
+ "category": "manual"
5524
+ },
5521
5525
  {
5522
5526
  "id": "manual:ownership",
5523
5527
  "category": "manual"
@@ -10929,8 +10933,8 @@
10929
10933
  "workflowRegistryPath": "dist/site/workflow-registry.json",
10930
10934
  "pageRegistryPath": "dist/site/page-registry.json",
10931
10935
  "cliRegistryDigest": "fbc23073bbae240489a2f826b2c26daadc58e704050c5f8df366509f5c5a1c3e",
10932
- "workflowRegistryDigest": "6391219ad0dae43ff217202a74a7e3d45669799cd142d590e9f0bafecaeb9912",
10933
- "pageRegistryDigest": "de1ba89591aeb5f0e24f1c5e455c21e8477e1094ec616ffddc2c1163f02c53b9"
10936
+ "workflowRegistryDigest": "e1bacd3ffb652ab8ef5c62986c4048d618e28ff43d4d03b61827adcce0e027be",
10937
+ "pageRegistryDigest": "178bc7609c831a63cecdb5baa817672a53393bb7d40f43b25fc8a736d94e2e9d"
10934
10938
  },
10935
10939
  "comparison": {
10936
10940
  "missingCliRegistry": [],
@@ -1356,10 +1356,7 @@
1356
1356
  },
1357
1357
  {
1358
1358
  "path": ".github/workflows/.release-candidate-promote.yml",
1359
- "signals": [
1360
- "write-permission",
1361
- "oidc"
1362
- ],
1359
+ "signals": [],
1363
1360
  "classified": true
1364
1361
  },
1365
1362
  {
@@ -1374,10 +1371,7 @@
1374
1371
  },
1375
1372
  {
1376
1373
  "path": ".github/workflows/.release-promote.yml",
1377
- "signals": [
1378
- "write-permission",
1379
- "oidc"
1380
- ],
1374
+ "signals": [],
1381
1375
  "classified": true
1382
1376
  },
1383
1377
  {
@@ -1723,10 +1717,7 @@
1723
1717
  },
1724
1718
  {
1725
1719
  "path": ".github/workflows/public-release-promote.yml",
1726
- "signals": [
1727
- "write-permission",
1728
- "oidc"
1729
- ],
1720
+ "signals": [],
1730
1721
  "classified": true
1731
1722
  },
1732
1723
  {
@@ -1768,10 +1759,7 @@
1768
1759
  },
1769
1760
  {
1770
1761
  "path": ".github/workflows/release-candidate-promote.yml",
1771
- "signals": [
1772
- "write-permission",
1773
- "oidc"
1774
- ],
1762
+ "signals": [],
1775
1763
  "classified": true
1776
1764
  },
1777
1765
  {
@@ -2021,5 +2009,5 @@
2021
2009
  "classified": true
2022
2010
  }
2023
2011
  ],
2024
- "registryDigest": "894cce314366698d41a3ab20172a0b7a05f03f2167667eae51793537b832c811"
2012
+ "registryDigest": "9c343ae9f9e778b608317d973b02cada327bd31e5149482d7f385b173c20ec14"
2025
2013
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-publication-release-registry",
4
- "generatedAt": "2026-09-06T13:21:07.000Z",
5
- "publishedAt": "2026-09-06T13:21:07.000Z",
4
+ "generatedAt": "2026-09-06T14:38:45.000Z",
5
+ "publishedAt": "2026-09-06T14:38:45.000Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "cf1b8e029ee3ef8b615cfc3c3814cfc8b7bf3c5c",
22
+ "sourceRevision": "7f41a21e534d41655d1c956bdc688e3167c2d340",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "package": {
34
34
  "name": "@kungfu-tech/buildchain",
35
- "version": "4.0.2-alpha.44",
35
+ "version": "4.0.2-alpha.45",
36
36
  "versionSource": "package.json#version"
37
37
  },
38
38
  "sourceKind": "package-site-bundle",
@@ -120,7 +120,8 @@
120
120
  "./site/product-mechanism.json": "./dist/site/product-mechanism.json",
121
121
  "./site/release-provenance.json": "./dist/site/release-provenance.json",
122
122
  "./site/agent-index.json": "./dist/site/agent-index.json",
123
- "./package.json": "./package.json"
123
+ "./package.json": "./package.json",
124
+ "./oci-publication": "./packages/core/oci-publication-bundle.js"
124
125
  }
125
126
  },
126
127
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-09-06T13:21:07.000Z",
5
- "publishedAt": "2026-09-06T13:21:07.000Z",
4
+ "generatedAt": "2026-09-06T14:38:45.000Z",
5
+ "publishedAt": "2026-09-06T14:38:45.000Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "cf1b8e029ee3ef8b615cfc3c3814cfc8b7bf3c5c",
22
+ "sourceRevision": "7f41a21e534d41655d1c956bdc688e3167c2d340",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "4.0.2-alpha.44",
40
+ "version": "4.0.2-alpha.45",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -53,7 +53,7 @@
53
53
  "path": "docs/MAP.md",
54
54
  "plane": "use",
55
55
  "exists": true,
56
- "digest": "sha256:aef677bb3e37882f73daf65b7052989fc549e3b40d70bc4036beb2738b608345"
56
+ "digest": "sha256:af7bee38e21a9ada99e1731f3576733f2897f9d19bdbf39eb2781650095ae419"
57
57
  },
58
58
  {
59
59
  "id": "getting-started",
@@ -253,7 +253,7 @@
253
253
  "path": "docs/node-api-reference.md",
254
254
  "plane": "use",
255
255
  "exists": true,
256
- "digest": "sha256:df073c14c12f15c851eb4507cbeb2919add936ac3079b10f222478138720d092"
256
+ "digest": "sha256:48232d47023b85063e7f779f053850e38149359ea3b2a5acd5126ff81f423ff4"
257
257
  },
258
258
  {
259
259
  "id": "build-facts",
@@ -2746,7 +2746,7 @@
2746
2746
  "status": "preview",
2747
2747
  "owner": "buildchain-workflows",
2748
2748
  "maturity": "preview",
2749
- "introducedVersion": "4.0.2-alpha.44",
2749
+ "introducedVersion": "4.0.2-alpha.45",
2750
2750
  "compatibilityPromise": "preserved-through-the-v3-major-line",
2751
2751
  "deprecationReplacement": "",
2752
2752
  "sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
@@ -3943,7 +3943,7 @@
3943
3943
  "status": "preview",
3944
3944
  "owner": "buildchain-workflows",
3945
3945
  "maturity": "preview",
3946
- "introducedVersion": "4.0.2-alpha.44",
3946
+ "introducedVersion": "4.0.2-alpha.45",
3947
3947
  "compatibilityPromise": "preserved-through-the-v3-major-line",
3948
3948
  "deprecationReplacement": "",
3949
3949
  "sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
@@ -5970,7 +5970,7 @@
5970
5970
  "status": "preview",
5971
5971
  "owner": "buildchain-workflows",
5972
5972
  "maturity": "preview",
5973
- "introducedVersion": "4.0.2-alpha.44",
5973
+ "introducedVersion": "4.0.2-alpha.45",
5974
5974
  "compatibilityPromise": "preserved-through-the-v3-major-line",
5975
5975
  "deprecationReplacement": "",
5976
5976
  "sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
@@ -6001,7 +6001,7 @@
6001
6001
  "status": "preview",
6002
6002
  "owner": "buildchain-workflows",
6003
6003
  "maturity": "preview",
6004
- "introducedVersion": "4.0.2-alpha.44",
6004
+ "introducedVersion": "4.0.2-alpha.45",
6005
6005
  "compatibilityPromise": "preserved-through-the-v3-major-line",
6006
6006
  "deprecationReplacement": "",
6007
6007
  "sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
@@ -6032,7 +6032,7 @@
6032
6032
  "status": "preview",
6033
6033
  "owner": "buildchain-workflows",
6034
6034
  "maturity": "preview",
6035
- "introducedVersion": "4.0.2-alpha.44",
6035
+ "introducedVersion": "4.0.2-alpha.45",
6036
6036
  "compatibilityPromise": "preserved-through-the-v3-major-line",
6037
6037
  "deprecationReplacement": "",
6038
6038
  "sunsetCondition": "explicit-breaking-change-review-in-a-future-major-line",
package/docs/MAP.md CHANGED
@@ -38,6 +38,7 @@ running artifact), *use* (consume / extend) - and a **status**:
38
38
  | CLI operator or agent | [Generated CLI Reference](cli-reference.md) | [CLI guide](cli.md) for concepts and worked examples |
39
39
  | Node toolkit developer | [Generated Node API Reference](node-api-reference.md) | [Core package guide](../packages/core/README.md) for recipes |
40
40
  | Workflow contributor | [Workflow Catalog](workflow-catalog.md) | [Contribution rules](../CONTRIBUTING.md#workflow-organization) and the [exact registry](../architecture/workflow-taxonomy.json) |
41
+ | Container image publisher | [OCI image family publication](oci-publication.md) | [Release Flow](release-flow.md) and [Runtime Train Validation](runtime-train-validation.md) |
41
42
  | Build/release operator | [Reusable Build Surface](reusable-build-surface.md) | [Release Flow](release-flow.md) and [Release Passport](release-passport.md) |
42
43
  | Product/trust reviewer | [Product Mechanism](product-mechanism.md) | [Release Passport](release-passport.md) and [KFD Support](kfd-support.md) |
43
44
 
@@ -2832,3 +2832,14 @@ Target: `./packages/core/buildchain-kfd-claims.js`. Public symbols: 10.
2832
2832
  | `createBuildchainKfdClaimRegistry` | function: function createBuildchainKfdClaimRegistry({ root = process.cwd(), sourceSha = "" } = {}) | { root = process.cwd(), sourceSha = "" } = {} | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { createBuildchainKfdClaimRegistry } from "@kungfu-tech/buildchain/buildchain-kfd-claims";` | `packages/core/buildchain-kfd-claims.js:510` |
2833
2833
  | `createBuildchainKfdSurfaceRegistry` | function: function createBuildchainKfdSurfaceRegistry({ root = process.cwd() } = {}) | { root = process.cwd() } = {} | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { createBuildchainKfdSurfaceRegistry } from "@kungfu-tech/buildchain/buildchain-kfd-claims";` | `packages/core/buildchain-kfd-claims.js:411` |
2834
2834
  | `createBuildchainPublicClaimDefinitions` | function: function createBuildchainPublicClaimDefinitions() | none | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { createBuildchainPublicClaimDefinitions } from "@kungfu-tech/buildchain/buildchain-kfd-claims";` | `packages/core/buildchain-kfd-claims.js:226` |
2835
+
2836
+ ## `@kungfu-tech/buildchain/oci-publication`
2837
+
2838
+ Target: `./packages/core/oci-publication-bundle.js`. Public symbols: 4.
2839
+
2840
+ | Symbol | Kind and signature | Parameters | Return | Errors | Side effects | Example | Source |
2841
+ | --- | --- | --- | --- | --- | --- | --- | --- |
2842
+ | `OCI_FAMILY_SCHEMA` | constant: const OCI_FAMILY_SCHEMA | none | value | Import does not declare a throw contract. | none-on-import | `import { OCI_FAMILY_SCHEMA } from "@kungfu-tech/buildchain/oci-publication";` | `packages/core/oci-publication-bundle.js:6` |
2843
+ | `ociBundleFile` | function: function ociBundleFile(root, relative) | root, relative | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { ociBundleFile } from "@kungfu-tech/buildchain/oci-publication";` | `packages/core/oci-publication-bundle.js:29` |
2844
+ | `sealOciPublicationBundle` | function: function sealOciPublicationBundle({ bundleRoot, body }) | { bundleRoot, body } | unknown | May throw an Error on rejected input or failed operations; follow the linked source contract. | none-detected-by-static-source-scan | `import { sealOciPublicationBundle } from "@kungfu-tech/buildchain/oci-publication";` | `packages/core/oci-publication-bundle.js:10` |
2845
+ | `verifyOciPublicationBundle` | function: function verifyOciPublicationBundle({ bundleRoot, manifest, repository, sourceSha, version, }) | { bundleRoot, manifest, repository, sourceSha, version, } | unknown | Errors from called operations may propagate; no narrower throw contract is declared in source. | none-detected-by-static-source-scan | `import { verifyOciPublicationBundle } from "@kungfu-tech/buildchain/oci-publication";` | `packages/core/oci-publication-bundle.js:114` |
@@ -0,0 +1,82 @@
1
+ ---
2
+ status: draft
3
+ period: ongoing
4
+ theme: oci-publication
5
+ doc_type: technical-reference
6
+ source_level: local-files
7
+ confidence: high
8
+ sensitivity: public
9
+ evidence_grade: B
10
+ review_state: unreviewed
11
+ last_reviewed: 2026-09-06
12
+ ai_provenance:
13
+ model_family: GPT-6
14
+ product: Codex
15
+ generated_at: 2026-09-06
16
+ invisible_context_boundary: No credentials or production registry state inspected.
17
+ ---
18
+
19
+ # OCI image family publication
20
+
21
+ Buildchain v4 can publish a sealed family of container images to GHCR through
22
+ `publish-artifact-kind: oci` on the public release candidate promotion workflow.
23
+ The candidate build produces the image bytes and smoke evidence. The built-in
24
+ provider verifies and uploads those bytes, reads back every public image digest,
25
+ then allows release references and GitHub Release evidence to converge.
26
+
27
+ ## Candidate contract
28
+
29
+ Publish exactly one `oci-family.json` alongside OCI layouts and smoke JSON files
30
+ in the Build artifact. Import `sealOciPublicationBundle` from
31
+ `@kungfu-tech/buildchain/oci-publication` to validate and seal the family. Its
32
+ `body` contains:
33
+
34
+ - `schema: kungfu-buildchain-oci-family/v1`, consumer `repository`, exact Git
35
+ `sourceSha`, and candidate package `version`.
36
+ - `expectedImages`: the complete set of image names.
37
+ - `images`: one entry for each expected image, with `name`, `repository`,
38
+ `digest`, `layout`, `platform`, `action`, `content`, and `smoke`.
39
+
40
+ Destinations must be `ghcr.io/<consumer-owner>/<consumer-repo>/<image-name>`.
41
+ `layout` is a relative OCI layout directory. Each image is a single Linux
42
+ amd64 or arm64 manifest; OCI and Docker schema 2 manifests are supported.
43
+ Layouts may share blobs and an index; each shared index entry identifies its
44
+ image with the `org.opencontainers.image.ref.name` annotation.
45
+
46
+ `content` records the image's original `sourceSha` and `version`, matching its
47
+ OCI revision/version labels. For `action: built`, both equal the candidate.
48
+ For `action: reused`, they retain the original content provenance while the
49
+ family binds the current candidate. `smoke` records a relative `path` and SHA256
50
+ `sha256`; its JSON must identify the image and record `passed: true`. The
51
+ consumer owns the actual smoke command and must fail the build if it fails.
52
+
53
+ Sealing verifies all config/layer sizes and digests, platform and provenance
54
+ labels, smoke bytes, and family completeness. Absolute paths, traversal, and
55
+ symlinks are rejected. The resolver derives the exact required image artifacts
56
+ from this sealed family, including when recovering a previous candidate run.
57
+
58
+ ## Authority and publication
59
+
60
+ The thin caller grants `packages: write` in addition to its existing promotion
61
+ permissions. Reusable promotion wrappers inherit that caller envelope; npm
62
+ consumers do not need to request package registry authority. The provider uses
63
+ the action token only for GHCR, scoped to each declared consumer image.
64
+ No consumer publish command is executed.
65
+
66
+ The rooted product plan declares version-state materialization, OCI family
67
+ publication, then release reference convergence. Each image uses the exact
68
+ `v<version>` tag. A different existing digest aborts publication; a matching
69
+ existing image is reused. After a partial failure, recovery validates the same
70
+ candidate bytes and publishes only missing images. GHCR does not provide tag
71
+ compare-and-swap: callers must serialize publishers for a version, and external
72
+ writers must not race the publication workflow.
73
+
74
+ Success requires anonymous readback of every declared image at its expected
75
+ digest. Existing GHCR packages must permit the repository token to write and
76
+ anonymous clients to pull. The provider does not change package visibility or
77
+ access policy. It attaches `oci-publication-readback.json` to the GitHub Release;
78
+ the caller should also include the family and smoke JSON files in
79
+ `github-release-payload-patterns`.
80
+
81
+ See [Runtime Train Validation](runtime-train-validation.md) for testing an
82
+ unreleased runtime without persisting a train or exact SHA in the workflow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "4.0.2-alpha.44",
3
+ "version": "4.0.2-alpha.45",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -123,7 +123,8 @@
123
123
  "./site/product-mechanism.json": "./dist/site/product-mechanism.json",
124
124
  "./site/release-provenance.json": "./dist/site/release-provenance.json",
125
125
  "./site/agent-index.json": "./dist/site/agent-index.json",
126
- "./package.json": "./package.json"
126
+ "./package.json": "./package.json",
127
+ "./oci-publication": "./packages/core/oci-publication-bundle.js"
127
128
  },
128
129
  "files": [
129
130
  "AGENTS.md",
@@ -0,0 +1,241 @@
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
+
6
+ export const OCI_FAMILY_SCHEMA = "kungfu-buildchain-oci-family/v1";
7
+ const digestPattern = /^sha256:[0-9a-f]{64}$/u;
8
+ const namePattern = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;
9
+
10
+ export function sealOciPublicationBundle({ bundleRoot, body }) {
11
+ const manifest = {
12
+ ...body,
13
+ root: v4ContentRoot("oci-publication-family", body),
14
+ };
15
+ verifyOciPublicationBundle({
16
+ bundleRoot,
17
+ manifest,
18
+ repository: body.repository,
19
+ sourceSha: body.sourceSha,
20
+ version: body.version,
21
+ });
22
+ return manifest;
23
+ }
24
+
25
+ function requireValue(condition, message) {
26
+ if (!condition) throw new Error(`OCI bundle: ${message}`);
27
+ }
28
+
29
+ export function ociBundleFile(root, relative) {
30
+ requireValue(
31
+ typeof relative === "string" &&
32
+ relative.length > 0 &&
33
+ !relative.includes("\\") &&
34
+ !path.isAbsolute(relative) &&
35
+ !relative.split(/[\\/]/u).some((p) => p === ".." || p === "." || !p),
36
+ "unsafe file path",
37
+ );
38
+ const base = fs.realpathSync(root);
39
+ let current = base;
40
+ for (const segment of relative.split("/")) {
41
+ current = path.join(current, segment);
42
+ requireValue(
43
+ !fs.lstatSync(current).isSymbolicLink(),
44
+ "symlinks are forbidden",
45
+ );
46
+ }
47
+ requireValue(
48
+ fs.statSync(current).isFile() &&
49
+ fs.realpathSync(current).startsWith(`${base}${path.sep}`),
50
+ "file escapes bundle",
51
+ );
52
+ return current;
53
+ }
54
+
55
+ function fileDigest(file) {
56
+ const hash = crypto.createHash("sha256"),
57
+ buffer = Buffer.alloc(1024 * 1024);
58
+ const fd = fs.openSync(file, "r");
59
+ try {
60
+ for (;;) {
61
+ const size = fs.readSync(fd, buffer);
62
+ if (!size) break;
63
+ hash.update(buffer.subarray(0, size));
64
+ }
65
+ } finally {
66
+ fs.closeSync(fd);
67
+ }
68
+ return `sha256:${hash.digest("hex")}`;
69
+ }
70
+
71
+ function verifyImageProvenance(image, config, sourceSha, version) {
72
+ const labels = config.config?.Labels || {};
73
+ requireValue(
74
+ image.content &&
75
+ /^[0-9a-f]{40}$/u.test(image.content.sourceSha) &&
76
+ typeof image.content.version === "string" &&
77
+ image.content.version.length > 0,
78
+ "missing image content provenance",
79
+ );
80
+ requireValue(
81
+ labels["org.opencontainers.image.revision"] === image.content.sourceSha &&
82
+ labels["org.opencontainers.image.version"] === image.content.version,
83
+ "image content provenance mismatch",
84
+ );
85
+ if (image.action === "built")
86
+ requireValue(
87
+ image.content.sourceSha === sourceSha &&
88
+ image.content.version === version,
89
+ "built image is not from the candidate source",
90
+ );
91
+ if (image.contractMajor !== undefined)
92
+ requireValue(
93
+ Number.isInteger(image.contractMajor) &&
94
+ image.contractMajor > 0 &&
95
+ labels["io.kungfu.image.contract-major"] ===
96
+ String(image.contractMajor),
97
+ "image contract major mismatch",
98
+ );
99
+ if (image.parentDigest)
100
+ requireValue(
101
+ digestPattern.test(image.parentDigest) &&
102
+ labels["io.kungfu.image.parent-digest"] === image.parentDigest,
103
+ "image parent digest mismatch",
104
+ );
105
+ if (image.content.materialSha)
106
+ requireValue(
107
+ /^[0-9a-f]{40}$/u.test(image.content.materialSha) &&
108
+ labels["io.kungfu.buildchain.release-material-sha"] ===
109
+ image.content.materialSha,
110
+ "image material provenance mismatch",
111
+ );
112
+ }
113
+
114
+ export function verifyOciPublicationBundle({
115
+ bundleRoot,
116
+ manifest,
117
+ repository,
118
+ sourceSha,
119
+ version,
120
+ }) {
121
+ requireValue(
122
+ manifest?.schema === OCI_FAMILY_SCHEMA,
123
+ "unsupported manifest schema",
124
+ );
125
+ const { root, ...body } = manifest;
126
+ const computed = v4ContentRoot("oci-publication-family", body);
127
+ requireValue(root === computed, "manifest root mismatch");
128
+ requireValue(
129
+ manifest.repository === repository &&
130
+ manifest.sourceSha === sourceSha &&
131
+ manifest.version === version,
132
+ "candidate identity mismatch",
133
+ );
134
+ requireValue(
135
+ /^[a-z0-9-]+\/[a-z0-9._-]+$/u.test(repository),
136
+ "invalid repository",
137
+ );
138
+ requireValue(
139
+ Array.isArray(manifest.images) && manifest.images.length > 0,
140
+ "empty image family",
141
+ );
142
+ const expected = manifest.expectedImages;
143
+ requireValue(
144
+ Array.isArray(expected) &&
145
+ expected.length === new Set(expected).size &&
146
+ expected.every((n) => namePattern.test(n)),
147
+ "invalid expected image family",
148
+ );
149
+ const actual = manifest.images.map((i) => i.name).sort();
150
+ requireValue(
151
+ JSON.stringify(actual) === JSON.stringify([...expected].sort()),
152
+ "incomplete or duplicate image family",
153
+ );
154
+ const files = new Map();
155
+ for (const image of manifest.images) {
156
+ requireValue(
157
+ image.repository === `ghcr.io/${repository}/${image.name}`,
158
+ "destination must belong to the consumer repository",
159
+ );
160
+ requireValue(
161
+ digestPattern.test(image.digest) &&
162
+ ["built", "reused"].includes(image.action),
163
+ "invalid image identity",
164
+ );
165
+ requireValue(
166
+ /^linux\/(amd64|arm64)$/u.test(image.platform),
167
+ "unsupported image platform",
168
+ );
169
+ const prefix = image.layout;
170
+ const descriptorFile = (descriptor) => {
171
+ requireValue(
172
+ digestPattern.test(descriptor.digest) &&
173
+ Number.isSafeInteger(descriptor.size) &&
174
+ descriptor.size >= 0,
175
+ "invalid OCI descriptor",
176
+ );
177
+ const relative = `${prefix}/blobs/sha256/${descriptor.digest.slice(7)}`;
178
+ const file = ociBundleFile(bundleRoot, relative);
179
+ requireValue(
180
+ fs.statSync(file).size === descriptor.size,
181
+ "OCI blob size mismatch",
182
+ );
183
+ if (!files.has(file))
184
+ requireValue(
185
+ fileDigest(file) === descriptor.digest,
186
+ "OCI blob digest mismatch",
187
+ );
188
+ files.set(file, descriptor);
189
+ return file;
190
+ };
191
+ const layout = JSON.parse(
192
+ fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/oci-layout`)),
193
+ );
194
+ requireValue(
195
+ layout.imageLayoutVersion === "1.0.0",
196
+ "unsupported OCI layout",
197
+ );
198
+ const index = JSON.parse(
199
+ fs.readFileSync(ociBundleFile(bundleRoot, `${prefix}/index.json`)),
200
+ );
201
+ const selected = (index.manifests || []).filter(
202
+ (descriptor) =>
203
+ descriptor.digest === image.digest &&
204
+ (descriptor.annotations?.["org.opencontainers.image.ref.name"] ===
205
+ image.name ||
206
+ index.manifests.length === 1),
207
+ );
208
+ requireValue(
209
+ index.schemaVersion === 2 && selected.length === 1,
210
+ "OCI index must select one exact image manifest",
211
+ );
212
+ const document = JSON.parse(fs.readFileSync(descriptorFile(selected[0])));
213
+ requireValue(
214
+ document.schemaVersion === 2 &&
215
+ [
216
+ "application/vnd.oci.image.manifest.v1+json",
217
+ "application/vnd.docker.distribution.manifest.v2+json",
218
+ ].includes(document.mediaType) &&
219
+ Array.isArray(document.layers),
220
+ "unsupported OCI image manifest",
221
+ );
222
+ const config = JSON.parse(fs.readFileSync(descriptorFile(document.config)));
223
+ requireValue(
224
+ `${config.os}/${config.architecture}` === image.platform,
225
+ "OCI platform mismatch",
226
+ );
227
+ verifyImageProvenance(image, config, sourceSha, version);
228
+ for (const layer of document.layers) descriptorFile(layer);
229
+ const smokeFile = ociBundleFile(bundleRoot, image.smoke.path);
230
+ requireValue(
231
+ fileDigest(smokeFile) === image.smoke.sha256,
232
+ "smoke evidence digest mismatch",
233
+ );
234
+ const smoke = JSON.parse(fs.readFileSync(smokeFile));
235
+ requireValue(
236
+ smoke.passed === true && smoke.image === image.name,
237
+ "smoke qualification failed",
238
+ );
239
+ }
240
+ return { root: computed, manifest, bundleRoot: fs.realpathSync(bundleRoot) };
241
+ }
@@ -17,6 +17,15 @@ export const RELEASE_TAIL_PRODUCT_CAPABILITIES = Object.freeze([
17
17
  receiptKind: "product-package-publication",
18
18
  transactionState: "publishing",
19
19
  }),
20
+ Object.freeze({
21
+ id: "product.oci.publish",
22
+ executor: "provider-adapter",
23
+ adapter: "oci-image-family",
24
+ effectKind: "oci-family-publication",
25
+ observationKind: "oci-family-readback",
26
+ receiptKind: "oci-family-publication",
27
+ transactionState: "publishing",
28
+ }),
20
29
  Object.freeze({
21
30
  id: "product.release-refs.converge",
22
31
  executor: "provider-adapter",
@@ -58,6 +58,7 @@ const ROOT_DOMAINS = new Set([
58
58
  "partial-mutation-recovery-checkpoint",
59
59
  "partial-mutation-recovery-plan",
60
60
  "v4-product-required-artifacts",
61
+ "oci-publication-family",
61
62
  "v4-product-publication-intent",
62
63
  "v4-product-publication-operation",
63
64
  "v4-product-publication-plan",
@@ -2,6 +2,6 @@
2
2
  export const V4_DOMAIN_WASM_ABI_VERSION = 1;
3
3
  export const V4_DOMAIN_WASM_RUSTC = "rustc 1.96.0 (ac68faa20 2026-05-25)";
4
4
  export const V4_DOMAIN_WASM_SOURCE_SHA256 =
5
- "07d45b2767e22007d3d41fa32c8a2dba28d9c88c11a2b28863e82d54be3a2933";
5
+ "7297279bce5344b8fc58688be5c1933a823306c82dac7dccfabdf07de41d652f";
6
6
  export const V4_DOMAIN_WASM_SHA256 =
7
- "b37c330e9366b7e9cf9dc4b7573b1e9495212dd9520e8b8ec6cdc3fd0ab7cafd";
7
+ "3a4909bb4b2bbd2676eac83f259c22faf1ce43479036992c5e53d5e2dcc512ce";
@@ -233,13 +233,21 @@ function workflowSnapshot(relative) {
233
233
  .map((job) => {
234
234
  const block = jobBlock(source, job.id);
235
235
  const uses = job.uses || null;
236
+ // A built-in publisher still requires mutation authority when the caller supplies its envelope.
237
+ const inheritedPublisher =
238
+ !/^ permissions:/mu.test(block) &&
239
+ /uses:.*actions\/v4-release-candidate-promote/u.test(block);
236
240
  return {
237
241
  id: job.id,
238
242
  kind: uses ? "reusable-call" : "runner",
239
243
  uses,
240
244
  permissions: {
241
- contents: permission(block, "contents"),
242
- idToken: permission(block, "id-token"),
245
+ contents:
246
+ permission(block, "contents") ||
247
+ (inheritedPublisher ? "inherited" : null),
248
+ idToken:
249
+ permission(block, "id-token") ||
250
+ (inheritedPublisher ? "inherited" : null),
243
251
  },
244
252
  carriers: {
245
253
  artifactDownload: /uses:\s+actions\/download-artifact@/u.test(block),
@@ -247,8 +255,10 @@ function workflowSnapshot(relative) {
247
255
  jobOutput: /GITHUB_OUTPUT/u.test(block),
248
256
  },
249
257
  mutationSignals: [
250
- /contents:\s*write/u.test(block) && "contents-write",
251
- /id-token:\s*write/u.test(block) && "oidc-write",
258
+ (/contents:\s*write/u.test(block) || inheritedPublisher) &&
259
+ "contents-write",
260
+ (/id-token:\s*write/u.test(block) || inheritedPublisher) &&
261
+ "oidc-write",
252
262
  /(?:promote-buildchain-ref|v4-release-candidate-promote)/u.test(
253
263
  block,
254
264
  ) && "promotion-runtime",