@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.
- package/actions/promote-buildchain-ref/README.md +11 -3
- package/bin/buildchain.mjs +78 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/buildchain-contract.json +163 -38
- package/dist/site/buildchain-site.json +73 -17
- package/dist/site/capability-registry.json +6 -5
- package/dist/site/cli-registry.json +36 -0
- package/dist/site/controller-registry.json +121 -10
- package/dist/site/kfd-claims.json +305 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +16 -2
- package/dist/site/node-api-registry.json +43 -4
- package/dist/site/page-registry.json +58 -11
- package/dist/site/public-surface-audit.json +212 -19
- package/dist/site/publication-authority-registry.json +778 -0
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +4 -0
- package/dist/site/site-manifest.json +15 -6
- package/dist/site/workflow-registry.json +173 -12
- package/docs/MAP.md +2 -1
- package/docs/publication-artifacts.md +25 -18
- package/docs/publication-authority.md +213 -0
- package/docs/release-governance.md +3 -1
- package/docs/shifu-gate-profiles.md +6 -0
- package/package.json +5 -1
- package/packages/core/buildchain-kfd-claims.js +5 -0
- package/packages/core/buildchain-publication-authority.js +80 -0
- package/packages/core/controller-evidence.js +8 -8
- package/packages/core/index.js +35 -0
- package/packages/core/publication-artifact-candidate.js +128 -0
- package/packages/core/publication-authority.js +764 -0
- package/packages/core/publication-control-plane-audit.js +135 -0
- package/scripts/assemble-publication-artifact-admission.mjs +190 -0
- package/scripts/assemble-self-publication-admission.mjs +183 -0
- package/scripts/audit-publication-control-plane.mjs +409 -0
- package/scripts/check-inventory.mjs +25 -2
- package/scripts/generate-site-bundle.mjs +16 -0
- package/scripts/publication-artifact-candidate.mjs +130 -0
- 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.
|
|
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";
|
package/packages/core/index.js
CHANGED
|
@@ -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
|
+
}
|