@kungfu-tech/buildchain 2.12.6 → 2.12.7-alpha.10

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 (40) hide show
  1. package/actions/promote-buildchain-ref/README.md +11 -3
  2. package/bin/buildchain.mjs +78 -0
  3. package/dist/site/agent-index.json +1 -0
  4. package/dist/site/artifact-schemas.json +1 -0
  5. package/dist/site/buildchain-contract.json +163 -38
  6. package/dist/site/buildchain-site.json +73 -17
  7. package/dist/site/capability-registry.json +6 -5
  8. package/dist/site/cli-registry.json +36 -0
  9. package/dist/site/controller-registry.json +121 -10
  10. package/dist/site/kfd-claims.json +305 -17
  11. package/dist/site/kfd-upstream-aggregate.json +1 -1
  12. package/dist/site/manual-registry.json +16 -2
  13. package/dist/site/node-api-registry.json +43 -4
  14. package/dist/site/page-registry.json +58 -11
  15. package/dist/site/public-surface-audit.json +212 -19
  16. package/dist/site/publication-authority-registry.json +778 -0
  17. package/dist/site/publication-registry.json +4 -4
  18. package/dist/site/release-provenance.json +4 -0
  19. package/dist/site/site-manifest.json +15 -6
  20. package/dist/site/workflow-registry.json +173 -12
  21. package/docs/MAP.md +2 -1
  22. package/docs/publication-artifacts.md +25 -18
  23. package/docs/publication-authority.md +213 -0
  24. package/docs/release-governance.md +3 -1
  25. package/docs/shifu-gate-profiles.md +6 -0
  26. package/package.json +5 -1
  27. package/packages/core/buildchain-kfd-claims.js +5 -0
  28. package/packages/core/buildchain-publication-authority.js +80 -0
  29. package/packages/core/controller-evidence.js +8 -8
  30. package/packages/core/index.js +35 -0
  31. package/packages/core/publication-artifact-candidate.js +128 -0
  32. package/packages/core/publication-authority.js +764 -0
  33. package/packages/core/publication-control-plane-audit.js +135 -0
  34. package/scripts/assemble-publication-artifact-admission.mjs +190 -0
  35. package/scripts/assemble-self-publication-admission.mjs +183 -0
  36. package/scripts/audit-publication-control-plane.mjs +409 -0
  37. package/scripts/check-inventory.mjs +25 -2
  38. package/scripts/generate-site-bundle.mjs +16 -0
  39. package/scripts/publication-artifact-candidate.mjs +130 -0
  40. package/scripts/workflow-friction-report.mjs +13 -2
@@ -0,0 +1,213 @@
1
+ ---
2
+ status: draft
3
+ period: ongoing
4
+ theme: sealed-publication-authority
5
+ doc_type: protocol
6
+ source_level: local-files
7
+ confidence: high
8
+ sensitivity: public
9
+ evidence_grade: B
10
+ review_state: unreviewed
11
+ last_reviewed: 2026-07-15
12
+ ai_provenance:
13
+ model_family: GPT-5
14
+ product: Codex
15
+ generated_at: 2026-07-15
16
+ limits: Live provider configuration must be re-audited; no credential values are represented.
17
+ ---
18
+
19
+ # Sealed Publication Authority
20
+
21
+ Buildchain publication authority is a closed-world, fail-closed protocol. It does
22
+ not mint registry or cloud credentials. It independently verifies whether an
23
+ already protected publication job is allowed to request a short-lived provider
24
+ credential for one exact product, target, version, channel, and artifact digest.
25
+
26
+ The machine-readable authority inventory is
27
+ `dist/site/publication-authority-registry.json`. Any workflow with a write,
28
+ environment, OIDC, cloud credential, registry publish, release, or Git push
29
+ signal must have an explicit descriptor. A new authority-bearing workflow that
30
+ is absent from the inventory fails site generation. Unknown workflows and every
31
+ descriptor not marked `product-publication` are denied product publication.
32
+
33
+ ## Evidence chain
34
+
35
+ A qualifying admission binds exact source and runtime SHAs; contract, consumer
36
+ policy, qualifying controller receipt, Shifu/Gate aggregate, artifact, runner,
37
+ and control-plane digests; repository, authority workflow, provider publisher
38
+ workflow, Environment policy,
39
+ product, target, version, and channel; plus a unique nonce and a lifetime of no
40
+ more than 15 minutes. Every expected binding is mandatory at verification time;
41
+ an omitted expected field is not a wildcard.
42
+
43
+ The independent verifier recomputes every digest and ignores a producer's own
44
+ allow/deny conclusion. It fetches the exact evidence run, validates the actual
45
+ release-candidate passport and referenced qualifying controller receipt,
46
+ recomputes the Shifu Gate aggregate or explicit consumer-owned no-Gate policy,
47
+ recomputes the downloaded artifact manifests, and hashes every declared product
48
+ payload file against those manifests. The `.buildchain/` diagnostics envelope is
49
+ bound by the manifest digest but excluded from the product-byte set because it can
50
+ be finalized after the lifecycle scan. The verifier also compares the PR evidence tree to the
51
+ admitted post-merge source commit tree. It rejects an unknown
52
+ workflow, stale or replayed nonce, runner downgrade, control-plane drift,
53
+ source/runtime mismatch, and artifact substitution. A successful result is a
54
+ scoped capability receipt, not a bearer credential.
55
+
56
+ ## Runner and control-plane evidence
57
+
58
+ Runner evidence uses exactly four classes: `ephemeral`, `reimaged`,
59
+ `persistent-measured`, and `unqualified`. Ephemeral runners also record their
60
+ job-isolation boundary. Reimaged and persistent runners qualify only when a
61
+ clean baseline is proven and baseline, toolchain, cache-contract, and task-
62
+ isolation digests are all present. Otherwise they still emit diagnostic
63
+ evidence with `qualificationStatus = unqualified`, but cannot receive a product
64
+ capability.
65
+
66
+ The external audit records digests and pass/fail status for repository Actions
67
+ defaults, classic branch protection or an active matching repository ruleset,
68
+ declared protected Environment policy or an explicit no-Environment binding,
69
+ job-scoped credentials,
70
+ absence of long-lived workflow publication credentials, provider authority,
71
+ and authorized runner class. Provider modes are `npm-trusted-publisher`,
72
+ `github-token`, and `oidc-role`. The OIDC-role mode consumes only a sanitized
73
+ provider audit containing a role digest and qualifying decision; raw IAM policy,
74
+ tokens, or credentials are rejected. Package-owner, cloud-root, GitHub
75
+ administrator, and registry-root credentials remain outside Buildchain's trust
76
+ boundary. Missing or unreadable facts fail closed.
77
+
78
+ An unauthenticated local npm CLI is not evidence that Trusted Publishing is
79
+ missing. `npm whoami` reports only the local CLI session and does not report the
80
+ OIDC identity that npm creates during `npm publish`. The default read-only audit
81
+ therefore binds the exact provider, repository, caller workflow, optional
82
+ Environment, job-scoped OIDC permission, and absence of long-lived credentials,
83
+ then records `provider-at-transaction`: npm makes the final authorization
84
+ decision when `npm publish` exchanges the job's OIDC token. A missing or drifted
85
+ trusted-publisher configuration consequently denies the transaction safely; it
86
+ is not preflighted through an unrelated long-lived npm login.
87
+
88
+ An authenticated external auditor can add stronger point-in-time evidence by
89
+ supplying sanitized `npm trust list --json` output with `--npm-trust-json`. This
90
+ changes the publisher fact to `audited-control-plane`; the workflow never runs
91
+ `npm trust list` itself and never receives that auditor's npm credential.
92
+
93
+ The credential-free collector proves effective Actions and runner scope from
94
+ the publication workflow fetched at `--workflow-ref`: explicit read-only
95
+ workflow defaults, job-scoped write/OIDC permissions, and an exact GitHub-hosted
96
+ runner label. It does not call repository Actions-default or self-hosted-runner
97
+ administration endpoints. Branch/ruleset and OIDC subject facts remain live
98
+ read-only provider queries. When the detailed branch-protection endpoint is not
99
+ readable with the workflow token, `--source-sha` binds the provider's public
100
+ protected-branch summary to the exact merged PR, independent approval, required
101
+ successful check, same-repository lineage, and current branch head. This records
102
+ provider-enforced transaction evidence without treating an unavailable
103
+ administration endpoint as an unprotected branch. It avoids turning a
104
+ repository-admin token into a publication prerequisite.
105
+
106
+ For non-dry-run workflows, missing admission, runner, control-plane, Gate, or
107
+ expected-binding evidence is rejected before Buildchain downloads candidate
108
+ artifacts. The denial explicitly records that npm Trusted Publishing and OIDC
109
+ were not evaluated, so downstream diagnostics cannot misclassify an admission
110
+ assembly failure as an npm authentication failure.
111
+
112
+ Buildchain's own `workflow_run` promotion lane may assemble those inputs only
113
+ for `kungfu-systems/buildchain`. It downloads the exact prior RC passport,
114
+ summary, referenced controller receipt, manifests, and product payloads; proves
115
+ the admitted channel commit has the same Git tree as the RC; performs the live
116
+ read-only control-plane audit; records the GitHub-hosted job as ephemeral runner
117
+ provenance; and creates an explicit Buildchain-owned no-Gate decision. The
118
+ independent verifier then recomputes every receipt and payload digest exactly as
119
+ it does for externally supplied admission. The self-assembly mode rejects other
120
+ repositories, unknown refs, non-exact source SHAs, and any caller other than
121
+ `.github/workflows/buildchain-ref-promotion.yml`. Manual apply and external
122
+ consumer workflows still require their own explicit admission inputs.
123
+
124
+ Before authority verification, the reusable promotion controller runs the same
125
+ release transaction selector in read-only mode. That plan supplies one exact
126
+ publication version and tag to the admission verifier, the publish-gate source
127
+ lock, and the real promotion action. The verifier rejects a capability for a
128
+ different version, and the promotion action rechecks the planned version before
129
+ any publish transaction side effect. Release-candidate fixture versions are
130
+ artifact evidence only; they never name Buildchain's own source-lock or
131
+ publication capability.
132
+
133
+ Evidence publication is a separate authority class and never grants product
134
+ publication.
135
+
136
+ ## API and CLI
137
+
138
+ Use `@kungfu-tech/buildchain/publication-authority` or run:
139
+
140
+ ```bash
141
+ buildchain verify publication-admission admission.json \
142
+ --registry-json publication-authority-registry.json \
143
+ --runner-json runner.json \
144
+ --control-plane-audit-json control-plane.json \
145
+ --publication-evidence-json publication-evidence.json \
146
+ --expected-json expected.json \
147
+ --used-nonce previous-run-nonce \
148
+ --json
149
+ ```
150
+
151
+ The read-only live collector defaults to npm trusted publishing. Other product
152
+ providers select an explicit adapter:
153
+
154
+ ```bash
155
+ buildchain audit publication-control-plane \
156
+ --repository kungfu-systems/buildchain \
157
+ --branch dev/v2/v2.12 \
158
+ --source-sha <exact-merged-branch-sha> \
159
+ --workflow .github/workflows/release-candidate-promote.yml \
160
+ --workflow-ref <exact-buildchain-sha> \
161
+ --publisher-workflow .github/workflows/buildchain-ref-promotion.yml \
162
+ --job promote \
163
+ --environment none
164
+
165
+ # Optional stronger external evidence; generate the JSON outside the workflow.
166
+ buildchain audit publication-control-plane \
167
+ --repository kungfu-systems/buildchain \
168
+ --branch dev/v2/v2.12 \
169
+ --workflow .github/workflows/release-candidate-promote.yml \
170
+ --publisher-workflow .github/workflows/buildchain-ref-promotion.yml \
171
+ --job promote \
172
+ --environment none \
173
+ --npm-trust-json sanitized-npm-trust.json
174
+
175
+ buildchain audit publication-control-plane \
176
+ --repository kungfu-systems/buildchain \
177
+ --branch release/v2/v2.12 \
178
+ --workflow .github/workflows/.binary-release-assets.yml \
179
+ --job publish \
180
+ --environment buildchain-release-assets \
181
+ --publisher-mode github-token
182
+
183
+ buildchain audit publication-control-plane \
184
+ --repository OWNER/CONSUMER \
185
+ --workflow-repository kungfu-systems/buildchain \
186
+ --branch main \
187
+ --workflow .github/workflows/.web-surface.yml \
188
+ --job production-apply \
189
+ --environment production \
190
+ --publisher-mode oidc-role \
191
+ --provider-audit-json sanitized-oidc-role-audit.json
192
+ ```
193
+
194
+ The authority workflow identifies the reusable implementation that performs the
195
+ publication job. The publisher workflow identifies the caller filename bound by
196
+ the provider's trusted-publisher policy; these identities are deliberately
197
+ separate. `--environment none` is an explicit assertion that the job declares no
198
+ GitHub Environment and the provider policy has no Environment restriction. A
199
+ named Environment must exist, be protected, and be declared by the job. The
200
+ Buildchain receipt alone is never sufficient authorization.
201
+
202
+ ## Publication lanes
203
+
204
+ `Binary Distribution` is evidence-only. It builds platform archives and a
205
+ release evidence bundle with read-only repository permissions. GitHub Release
206
+ asset writes live in `Binary Release Assets`, which downloads an exact prior
207
+ evidence run, verifies its bundle digest against the sealed capability, and is
208
+ the only binary job with `contents: write` in the protected
209
+ `buildchain-release-assets` Environment.
210
+
211
+ The npm/promotion, paper, binary-release, and web-production lanes all depend on
212
+ the independent verifier. Preview, staging, build, source-check, controller,
213
+ and failure-evidence lanes do not inherit product publication capability.
@@ -627,7 +627,9 @@ passes `require-publish-source-lock`, `publish-source-ref`,
627
627
  `promote-buildchain-ref`. Direct action callers must pass the same four inputs
628
628
  from the reusable build outputs. Workflows that only collect passports or run
629
629
  dry-run package checks do not move publish refs and are not publish-gate
630
- publication models.
630
+ publication models. A dry-run of `release-candidate-promote.yml` computes and
631
+ reports the exact `publish-gate/*` source lock that a real promotion would use,
632
+ but does not read, create, or move that ref.
631
633
 
632
634
  Semver GitHub Release publication is owned by `promote-buildchain-ref`, not by
633
635
  consumer shell glue. Consumers normally use the `release-candidate-promote.yml`
@@ -78,6 +78,12 @@ Every matrix job checks out the exact source SHA planned by Buildchain, invokes
78
78
  then invokes `shifu gate receipt validate`. Buildchain uploads the original
79
79
  receipt and validation result even when the run fails.
80
80
 
81
+ Before invoking the project-owned command, the workflow adds the runner
82
+ account's `~/.local/bin` directory to `PATH` on Windows, Linux, and macOS. This
83
+ keeps user-scoped tools such as `uv` available to strict Shifu cache profiles
84
+ without assuming an administrator-managed system installation. The consumer or
85
+ runner owner remains responsible for provisioning the declared tools.
86
+
81
87
  The fixed `Gate profile / aggregate` job fails closed for missing receipts,
82
88
  invalid or stale Shifu validation, dirty or mismatched source SHA, registry or
83
89
  plan drift, missing required results, required failures/skips, or gate action
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.12.6",
3
+ "version": "2.12.7-alpha.10",
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",
@@ -29,6 +29,9 @@
29
29
  "./logging": "./packages/core/logging.js",
30
30
  "./publication-artifact": "./packages/core/publication-artifact.js",
31
31
  "./publication-package": "./packages/core/publication-package.js",
32
+ "./publication-authority": "./packages/core/publication-authority.js",
33
+ "./publication-control-plane-audit": "./packages/core/publication-control-plane-audit.js",
34
+ "./buildchain-publication-authority": "./packages/core/buildchain-publication-authority.js",
32
35
  "./kfd-gate": "./packages/core/kfd-gate.js",
33
36
  "./release-candidate": "./packages/core/release-candidate.js",
34
37
  "./stable-candidate-ledger": "./packages/core/stable-candidate-ledger.js",
@@ -47,6 +50,7 @@
47
50
  "./site/kfd-claims.json": "./dist/site/kfd-claims.json",
48
51
  "./site/workflow-registry.json": "./dist/site/workflow-registry.json",
49
52
  "./site/controller-registry.json": "./dist/site/controller-registry.json",
53
+ "./site/publication-authority-registry.json": "./dist/site/publication-authority-registry.json",
50
54
  "./site/public-surface-audit.json": "./dist/site/public-surface-audit.json",
51
55
  "./site/kfd-upstream-aggregate.json": "./dist/site/kfd-upstream-aggregate.json",
52
56
  "./site/release-model.json": "./dist/site/release-model.json",
@@ -16,6 +16,7 @@ export const BUILDCHAIN_AGENT_MANUALS = Object.freeze([
16
16
  { id: "install", title: "Install and verify Buildchain", path: "docs/install.md", plane: "use" },
17
17
  { id: "release-passport", title: "Release Passport protocol", path: "docs/release-passport.md", plane: "verify" },
18
18
  { id: "controller-evidence", title: "Controller evidence contract", path: "docs/controller-evidence.md", plane: "verify" },
19
+ { id: "publication-authority", title: "Sealed publication authority", path: "docs/publication-authority.md", plane: "verify" },
19
20
  { id: "release-candidate", title: "Release Candidate Passport", path: "docs/release-candidate.md", plane: "verify" },
20
21
  { id: "release-propagation", title: "Release propagation", path: "docs/release-propagation.md", plane: "use" },
21
22
  { id: "readme-badges", title: "README badge blocks", path: "docs/readme-badges.md", plane: "use" },
@@ -52,6 +53,7 @@ const SITE_CONTRACT_FILES = Object.freeze([
52
53
  "dist/site/node-api-registry.json",
53
54
  "dist/site/workflow-registry.json",
54
55
  "dist/site/controller-registry.json",
56
+ "dist/site/publication-authority-registry.json",
55
57
  "dist/site/public-surface-audit.json",
56
58
  "dist/site/release-model.json",
57
59
  "dist/site/artifact-schemas.json",
@@ -68,6 +70,9 @@ const SCHEMA_AND_STANDARD_FILES = Object.freeze([
68
70
  "packages/core/release-passport.js",
69
71
  "packages/core/buildchain-contract.js",
70
72
  "packages/core/controller-evidence.js",
73
+ "packages/core/publication-authority.js",
74
+ "packages/core/publication-control-plane-audit.js",
75
+ "packages/core/buildchain-publication-authority.js",
71
76
  "packages/core/buildchain-kfd-claims.js",
72
77
  "packages/core/kfd3-surface-register.js",
73
78
  "packages/core/public-surface-audit.js",
@@ -0,0 +1,80 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { createPublicationAuthorityRegistry } from "./publication-authority.js";
5
+
6
+ const DESCRIPTORS = Object.freeze([
7
+ [".github/workflows/.build.yml", "non-publication-oidc"],
8
+ [".github/workflows/.publication-authority.yml", "evidence-publication"],
9
+ [".github/workflows/.release-verify.yml", "governance-write"],
10
+ [".github/workflows/.sam-release.yml", "retired-deny"],
11
+ [".github/workflows/.sam-verify.yml", "retired-deny"],
12
+ [".github/workflows/.sync-remote-git.yml", "governance-write"],
13
+ [".github/workflows/.web-surface.yml", "product-publication", true, ["web-production"], "oidc", "consumer-defined", "caller-bound", "caller-bound"],
14
+ [".github/workflows/.wheel-release.yml", "retired-deny"],
15
+ [".github/workflows/.wheel-verify.yml", "retired-deny"],
16
+ [".github/workflows/.binary-release-assets.yml", "product-publication", true, ["github-release"], "github-token", "buildchain-release-assets"],
17
+ [".github/workflows/binary-distribution.yml", "evidence-publication"],
18
+ [".github/workflows/binary-release-assets.yml", "governance-write"],
19
+ [".github/workflows/build-surface-fixture.yml", "non-publication-oidc"],
20
+ [".github/workflows/build.yml", "non-publication-oidc"],
21
+ [".github/workflows/buildchain-alpha-self-dogfood.yml", "non-publication-oidc"],
22
+ [".github/workflows/buildchain-patrol-daily.yml", "governance-write"],
23
+ [".github/workflows/buildchain-patrol-monthly.yml", "governance-write"],
24
+ [".github/workflows/buildchain-patrol-weekly.yml", "governance-write"],
25
+ [".github/workflows/buildchain-patrol.yml", "governance-write"],
26
+ [".github/workflows/buildchain-ref-promotion.yml", "governance-write"],
27
+ [".github/workflows/buildchain-stable-candidate-patrol.yml", "governance-write"],
28
+ [".github/workflows/dev-pr-auto-merge.yml", "governance-write"],
29
+ [".github/workflows/npm-publish.yml", "dry-run-only"],
30
+ [".github/workflows/paper-release-sealed.yml", "product-publication", true, ["npm-publish", "github-release"], "trusted-publishing", "none", "fixed", "caller-bound"],
31
+ [".github/workflows/paper-release.yml", "product-publication", true, ["npm-publish", "github-release"], "trusted-publishing", "none", "fixed", "caller-bound"],
32
+ [".github/workflows/patrol-daily.yml", "governance-write"],
33
+ [".github/workflows/patrol-monthly.yml", "governance-write"],
34
+ [".github/workflows/patrol-weekly.yml", "governance-write"],
35
+ [".github/workflows/publication-artifact.yml", "evidence-publication"],
36
+ [".github/workflows/release-candidate-promote.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "caller-bound"],
37
+ [".github/workflows/release-line-bootstrap.yml", "governance-write"],
38
+ [".github/workflows/release-propagation.yml", "governance-write"],
39
+ [".github/workflows/release-verify.yml", "governance-write"],
40
+ [".github/workflows/stable-candidate-patrol.yml", "governance-write"],
41
+ ]);
42
+
43
+ export function buildchainPublicationAuthorityDescriptors() {
44
+ return DESCRIPTORS.map(([
45
+ workflowPath,
46
+ authorityClass,
47
+ publicationCapable = false,
48
+ capabilityIds = [],
49
+ credentialMode = "none",
50
+ environment = "",
51
+ environmentMode = "fixed",
52
+ publisherWorkflowMode = "fixed",
53
+ publisherWorkflowPath = workflowPath,
54
+ ]) => ({
55
+ workflowPath,
56
+ authorityClass,
57
+ publicationCapable,
58
+ capabilityIds,
59
+ credentialMode,
60
+ publisherWorkflowMode,
61
+ publisherWorkflowPath,
62
+ environment,
63
+ environmentMode,
64
+ runnerPolicy: publicationCapable ? "qualified-measured" : "unqualified",
65
+ }));
66
+ }
67
+
68
+ export function createBuildchainPublicationAuthorityRegistry({ root = process.cwd() } = {}) {
69
+ const workflowsDir = path.join(root, ".github", "workflows");
70
+ const workflows = fs.readdirSync(workflowsDir)
71
+ .filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
72
+ .map((name) => ({
73
+ path: `.github/workflows/${name}`,
74
+ text: fs.readFileSync(path.join(workflowsDir, name), "utf8"),
75
+ }));
76
+ return createPublicationAuthorityRegistry({
77
+ descriptors: buildchainPublicationAuthorityDescriptors(),
78
+ workflows,
79
+ });
80
+ }
@@ -47,9 +47,9 @@ const CONTROLLER_SPECS = [
47
47
  id: "web-surface",
48
48
  workflowId: ".web-surface",
49
49
  version: 1,
50
- capabilities: ["web-build", "web-verify", "deployment-plan", "deployment-apply"],
51
- stages: ["resolve-runtime", "plan", "build", "verify", "apply", "aggregate"],
52
- optionalStages: ["build", "verify", "apply"],
50
+ capabilities: ["web-build", "web-verify", "deployment-plan", "deployment-apply", "publication-authority"],
51
+ stages: ["resolve-runtime", "plan", "build", "verify", "publication-authority", "apply", "aggregate"],
52
+ optionalStages: ["build", "verify", "publication-authority", "apply"],
53
53
  evidence: ["web-surface-plan", "controller-receipt"],
54
54
  },
55
55
  {
@@ -65,8 +65,8 @@ const CONTROLLER_SPECS = [
65
65
  id: "paper-release",
66
66
  workflowId: "paper-release",
67
67
  version: 1,
68
- capabilities: ["publication-build", "artifact-admission", "release-publish", "release-passport"],
69
- stages: ["resolve-runtime", "build", "verify", "collect", "publish", "passport", "aggregate"],
68
+ capabilities: ["publication-build", "artifact-admission", "publication-authority", "release-publish", "release-passport"],
69
+ stages: ["resolve-runtime", "build", "verify", "collect", "publication-authority", "publish", "passport", "aggregate"],
70
70
  optionalStages: ["verify"],
71
71
  evidence: ["publication-manifest", "release-passport", "controller-receipt"],
72
72
  },
@@ -74,8 +74,8 @@ const CONTROLLER_SPECS = [
74
74
  id: "release-candidate-promotion",
75
75
  workflowId: "release-candidate-promote",
76
76
  version: 1,
77
- capabilities: ["promotion-preflight", "artifact-admission", "publish-transaction", "release-passport"],
78
- stages: ["preflight", "admit-release-candidate", "publish", "passport", "aggregate"],
77
+ capabilities: ["promotion-preflight", "artifact-admission", "publication-authority", "publish-transaction", "release-passport"],
78
+ stages: ["preflight", "admit-release-candidate", "publication-authority", "publish", "passport", "aggregate"],
79
79
  evidence: ["release-candidate-passport", "publish-evidence", "release-passport", "controller-receipt"],
80
80
  },
81
81
  {
@@ -303,7 +303,7 @@ function normalizeEvidence(entries = []) {
303
303
  function receiptStatus(plan, stages) {
304
304
  if (stages.some((stage) => stage.status === "failed")) return "failed";
305
305
  if (stages.length > 0 && stages.every((stage) => stage.status === "skipped")) return "skipped";
306
- if (stages.some((stage) => stage.status === "cancelled" || stage.status === "missing")) return "partial";
306
+ if (stages.some((stage) => stage.status === "cancelled" || (stage.required && stage.status === "missing"))) return "partial";
307
307
  const required = new Set(plan.expected.stages.filter((stage) => stage.required).map((stage) => stage.id));
308
308
  if (stages.filter((stage) => required.has(stage.id)).every((stage) => stage.status === "passed")) return "passed";
309
309
  return "partial";
@@ -132,6 +132,41 @@ export {
132
132
  preparePublicationNpmPackage,
133
133
  } from "./publication-package.js";
134
134
 
135
+ export {
136
+ PUBLICATION_ADMISSION_CONTRACT,
137
+ PUBLICATION_ARTIFACT_MANIFEST_SET_CONTRACT,
138
+ PUBLICATION_AUTHORITY_CLASSES,
139
+ PUBLICATION_AUTHORITY_REGISTRY_CONTRACT,
140
+ PUBLICATION_CAPABILITY_CONTRACT,
141
+ PUBLICATION_CONTROL_PLANE_AUDIT_CONTRACT,
142
+ PUBLICATION_GATE_DECISION_CONTRACT,
143
+ RUNNER_PROVENANCE_CLASSES,
144
+ RUNNER_PROVENANCE_CONTRACT,
145
+ createPublicationAuthorityRegistry,
146
+ createPublicationAdmission,
147
+ createPublicationArtifactManifestSet,
148
+ createPublicationControlPlaneAudit,
149
+ createPublicationGateDecision,
150
+ createRunnerProvenance,
151
+ detectPublicationAuthoritySignals,
152
+ publicationAuthorityDigest,
153
+ verifyPublicationAdmission,
154
+ } from "./publication-authority.js";
155
+
156
+ export {
157
+ PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
158
+ createPublicationArtifactCandidate,
159
+ publicationArtifactCandidateDigest,
160
+ resolvePublicationCandidateFile,
161
+ } from "./publication-artifact-candidate.js";
162
+
163
+ export {
164
+ buildchainPublicationAuthorityDescriptors,
165
+ createBuildchainPublicationAuthorityRegistry,
166
+ } from "./buildchain-publication-authority.js";
167
+
168
+ export { evaluatePublicationControlPlaneSnapshot } from "./publication-control-plane-audit.js";
169
+
135
170
  export {
136
171
  ARTIFACT_PASSPORT_LOCATOR_CONTRACT,
137
172
  ARTIFACT_PASSPORT_POINTER_CONTRACT,
@@ -0,0 +1,128 @@
1
+ import crypto from "node:crypto";
2
+
3
+ import { validateControllerReceipt } from "./controller-evidence.js";
4
+
5
+ export const PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT =
6
+ "kungfu-buildchain-publication-artifact-candidate";
7
+
8
+ const MANIFEST_CONTRACT = "kungfu-buildchain-publication-artifact-manifest";
9
+ const PASSPORT_CONTRACT = "kungfu-buildchain-publication-artifact-passport";
10
+
11
+ function stableJson(value) {
12
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
13
+ if (value && typeof value === "object") {
14
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
15
+ }
16
+ return JSON.stringify(value);
17
+ }
18
+
19
+ export function publicationArtifactCandidateDigest(value) {
20
+ return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
21
+ }
22
+
23
+ export function resolvePublicationCandidateFile(files = [], candidatePath) {
24
+ const normalizedPath = requiredString(candidatePath, "candidatePath").replaceAll("\\", "/");
25
+ if (normalizedPath.startsWith("/") || normalizedPath.split("/").includes("..")) {
26
+ throw new Error(`publication artifact candidate contains an unsafe path: ${normalizedPath}`);
27
+ }
28
+ const matches = files.filter((entry) => entry.path === normalizedPath);
29
+ if (matches.length !== 1) {
30
+ throw new Error(`expected exactly one publication candidate file at ${normalizedPath}, found ${matches.length}`);
31
+ }
32
+ return matches[0].path;
33
+ }
34
+
35
+ function requiredString(value, label) {
36
+ const normalized = String(value || "").trim();
37
+ if (!normalized) throw new Error(`${label} must be a non-empty string`);
38
+ return normalized;
39
+ }
40
+
41
+ function normalizeDigest(value, label) {
42
+ const normalized = requiredString(value, label).replace(/^sha256:/, "").toLowerCase();
43
+ if (!/^[0-9a-f]{64}$/.test(normalized)) throw new Error(`${label} must be a sha256 digest`);
44
+ return normalized;
45
+ }
46
+
47
+ function normalizeGitSha(value, label) {
48
+ const normalized = requiredString(value, label).toLowerCase();
49
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(normalized)) {
50
+ throw new Error(`${label} must be a 40- or 64-character Git SHA`);
51
+ }
52
+ return normalized;
53
+ }
54
+
55
+ export function createPublicationArtifactCandidate({
56
+ repository,
57
+ sourceSha,
58
+ sourceTreeSha,
59
+ runtimeSha,
60
+ manifest,
61
+ passport,
62
+ controllerReceipt,
63
+ files = [],
64
+ } = {}) {
65
+ const normalizedRepository = requiredString(repository, "repository");
66
+ const normalizedSourceSha = normalizeGitSha(sourceSha, "sourceSha");
67
+ const normalizedSourceTreeSha = normalizeGitSha(sourceTreeSha, "sourceTreeSha");
68
+ const normalizedRuntimeSha = normalizeGitSha(runtimeSha, "runtimeSha");
69
+ if (manifest?.contract !== MANIFEST_CONTRACT) throw new Error("publication artifact manifest contract mismatch");
70
+ if (passport?.contract !== PASSPORT_CONTRACT || passport.status !== "passed") {
71
+ throw new Error("publication artifact passport is not qualifying");
72
+ }
73
+ if (manifest.source?.sha !== normalizedSourceSha || passport.source?.sha !== normalizedSourceSha) {
74
+ throw new Error("publication artifact source SHA mismatch");
75
+ }
76
+ if (manifest.source?.treeSha !== normalizedSourceTreeSha || passport.source?.treeSha !== normalizedSourceTreeSha) {
77
+ throw new Error("publication artifact source tree mismatch");
78
+ }
79
+ const manifestDigest = crypto.createHash("sha256").update(JSON.stringify(manifest, null, 2)).digest("hex");
80
+ if (normalizeDigest(passport.manifestDigest, "passport.manifestDigest") !== manifestDigest) {
81
+ throw new Error("publication artifact passport manifest digest mismatch");
82
+ }
83
+ const controllerValidation = validateControllerReceipt(controllerReceipt, {
84
+ expectedSourceSha: normalizedSourceSha,
85
+ expectedRuntimeSha: normalizedRuntimeSha,
86
+ });
87
+ if (!controllerValidation.ok || !controllerValidation.qualifying) {
88
+ throw new Error(`publication artifact controller receipt did not qualify: ${controllerValidation.issues.join("; ")}`);
89
+ }
90
+ const evidenceKinds = new Set((controllerReceipt.evidence || []).map((entry) => entry.kind));
91
+ for (const kind of ["publication-manifest", "publication-passport"]) {
92
+ if (!evidenceKinds.has(kind)) throw new Error(`publication artifact controller receipt is missing ${kind} evidence`);
93
+ }
94
+ const normalizedFiles = files.map((entry, index) => {
95
+ const filePath = requiredString(entry.path, `files[${index}].path`).replaceAll("\\", "/");
96
+ if (filePath.startsWith("/") || filePath.split("/").includes("..")) {
97
+ throw new Error(`publication artifact candidate contains an unsafe path: ${filePath}`);
98
+ }
99
+ const size = Number(entry.size ?? entry.bytes);
100
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error(`files[${index}].size must be a non-negative safe integer`);
101
+ return { path: filePath, size, sha256: normalizeDigest(entry.sha256, `files[${index}].sha256`) };
102
+ }).sort((left, right) => left.path.localeCompare(right.path));
103
+ if (new Set(normalizedFiles.map((entry) => entry.path)).size !== normalizedFiles.length) {
104
+ throw new Error("publication artifact candidate file paths must be unique");
105
+ }
106
+ const byPath = new Map(normalizedFiles.map((entry) => [entry.path, entry]));
107
+ for (const [index, artifact] of (manifest.artifacts || []).entries()) {
108
+ const artifactPath = requiredString(artifact.path, `manifest.artifacts[${index}].path`);
109
+ const actual = byPath.get(artifactPath);
110
+ if (!actual) throw new Error(`publication artifact candidate is missing declared artifact: ${artifactPath}`);
111
+ if (actual.size !== Number(artifact.bytes) || actual.sha256 !== normalizeDigest(artifact.sha256, `manifest.artifacts[${index}].sha256`)) {
112
+ throw new Error(`publication artifact candidate bytes do not match manifest: ${artifactPath}`);
113
+ }
114
+ }
115
+ const payload = {
116
+ schemaVersion: 1,
117
+ contract: PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
118
+ repository: normalizedRepository,
119
+ sourceSha: normalizedSourceSha,
120
+ sourceTreeSha: normalizedSourceTreeSha,
121
+ runtimeSha: normalizedRuntimeSha,
122
+ manifestDigest,
123
+ passportDigest: publicationArtifactCandidateDigest(passport),
124
+ controllerReceiptDigest: normalizeDigest(controllerReceipt.digest, "controllerReceipt.digest"),
125
+ files: normalizedFiles,
126
+ };
127
+ return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
128
+ }