@kungfu-tech/buildchain 4.0.0 → 4.0.1-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -5
- package/actions/promote-buildchain-ref/README.md +11 -6
- package/architecture/internal-capabilities.json +11 -2
- package/architecture/maintainability-policy.json +149 -17
- package/architecture/v4-floating-consumer-policy.json +75 -0
- package/architecture/v4-floating-consumer-policy.md +32 -0
- package/architecture/v4-runtime-ref-resume-authority.json +64 -0
- package/architecture/v4-stage-capsule-qualification.json +2 -2
- package/bin/internal/trust-release-release-handlers.mjs +5 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +53 -0
- package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
- package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +105 -0
- package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
- package/dist/site/buildchain-contract.json +101 -31
- package/dist/site/buildchain-site.json +73 -27
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/controller-registry.json +62 -6
- package/dist/site/kfd-claims.json +68 -13
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +799 -127
- package/dist/site/page-registry.json +63 -17
- package/dist/site/public-surface-audit.json +49 -17
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +10 -10
- package/dist/site/workflow-registry.json +46 -18
- package/docs/MAP.md +2 -0
- package/docs/dev-delivery-warrant.md +19 -1
- package/docs/lifecycle-protocol.md +41 -0
- package/docs/node-api-reference.md +207 -134
- package/docs/reusable-build-surface.md +41 -0
- package/docs/runtime-train-validation.md +10 -0
- package/docs/v4-runtime-ref-resume-authority.md +69 -0
- package/docs/versioning.md +1 -0
- package/package.json +6 -4
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-config.js +66 -1
- package/packages/core/dev-delivery-proof.js +37 -3
- package/packages/core/dev-delivery-warrant.js +3 -3
- package/packages/core/index.js +2 -0
- package/packages/core/publication-authority.js +5 -5
- package/packages/core/release-candidate.js +79 -19
- package/packages/core/release-passport.js +239 -33
- package/packages/core/v4-floating-consumer-evidence.js +324 -0
- package/packages/core/v4-floating-consumer-policy.js +446 -0
- package/packages/core/v4-floating-consumer-release-passport.js +126 -0
- package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
- package/packages/core/v4-runtime-selector-persistence.js +228 -0
- package/packages/core/workflow-yaml-contract.js +48 -0
- package/scripts/audit-publication-control-plane.mjs +31 -31
- package/scripts/check-inventory.mjs +1 -1
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +198 -0
- package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
- package/scripts/dev-delivery-source-proof-reuse.mjs +631 -0
- package/scripts/dev-pr-auto-merge.mjs +37 -48
- package/scripts/dev-pr-delivery-warrant.mjs +205 -2
- package/scripts/dev-pr-prequeue-guard.mjs +399 -0
- package/scripts/ensure-github-release.mjs +3 -3
- package/scripts/generate-channel-build-workflow.mjs +3 -0
- package/scripts/generate-channel-promotion-workflow.mjs +112 -12
- package/scripts/generate-release-candidate-passport.mjs +41 -33
- package/scripts/init-repo.mjs +5 -1
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/npm-publish-transaction.mjs +101 -89
- package/scripts/resume-from-candidate-run.mjs +11 -14
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +2 -0
- package/scripts/v4-consumer-policy.mjs +231 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseYamlUses } from "./workflow-yaml-contract.js";
|
|
5
|
+
import {
|
|
6
|
+
V4_FLOATING_CONSUMER_POLICY,
|
|
7
|
+
V4_FLOATING_CONSUMER_RECEIPT,
|
|
8
|
+
v4FloatingConsumerDocumentRoot,
|
|
9
|
+
} from "./v4-floating-consumer-evidence.js";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
V4_FLOATING_CONSUMER_CERTIFICATION,
|
|
13
|
+
V4_FLOATING_CONSUMER_POLICY,
|
|
14
|
+
V4_FLOATING_CONSUMER_RECEIPT,
|
|
15
|
+
certifyV4FloatingConsumerPolicyReceipt,
|
|
16
|
+
v4FloatingConsumerDocumentRoot,
|
|
17
|
+
verifyV4FloatingConsumerPolicyCertification,
|
|
18
|
+
verifyV4FloatingConsumerPolicyReceipt,
|
|
19
|
+
} from "./v4-floating-consumer-evidence.js";
|
|
20
|
+
|
|
21
|
+
const EXACT_SHA = /^[0-9a-f]{40}$/u;
|
|
22
|
+
const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
23
|
+
const BUILDCHAIN_REPOSITORY = "kungfu-systems/buildchain";
|
|
24
|
+
const CHANNELS = Object.freeze({ v4: "stable", "v4-alpha": "alpha" });
|
|
25
|
+
const DEFAULT_STABLE_LOCK_PATH = ".buildchain/contract-lock.json";
|
|
26
|
+
const DEFAULT_ALPHA_LOCK_PATH = ".buildchain/alpha-contract-lock.json";
|
|
27
|
+
const TRANSIENT_ACTION =
|
|
28
|
+
/^\.\/\.buildchain\/(?:runtime|workflow-shell|attester-runtime|release-tail-runtime)\//u;
|
|
29
|
+
const SCANNER_PATHS = Object.freeze([
|
|
30
|
+
"architecture/v4-floating-consumer-policy.json",
|
|
31
|
+
"contracts/v4-floating-consumer-policy-receipt-v1.schema.json",
|
|
32
|
+
"packages/core/v4-floating-consumer-policy.js",
|
|
33
|
+
"packages/core/workflow-yaml-contract.js",
|
|
34
|
+
"scripts/v4-consumer-policy.mjs",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function defaultRuntimeRoot() {
|
|
38
|
+
return typeof import.meta.dirname === "string"
|
|
39
|
+
? path.resolve(import.meta.dirname, "../..")
|
|
40
|
+
: process.cwd();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableJson(value) {
|
|
44
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
return `{${Object.keys(value)
|
|
47
|
+
.sort()
|
|
48
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
49
|
+
.join(",")}}`;
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sha256(value) {
|
|
55
|
+
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveInside(root, relative, label) {
|
|
59
|
+
const resolvedRoot = path.resolve(root);
|
|
60
|
+
const file = path.resolve(resolvedRoot, relative);
|
|
61
|
+
if (!file.startsWith(`${resolvedRoot}${path.sep}`))
|
|
62
|
+
throw new Error(`${label} escapes caller root: ${relative}`);
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function v4ConsumerPolicyScannerRoot(
|
|
67
|
+
runtimeRoot = defaultRuntimeRoot(),
|
|
68
|
+
) {
|
|
69
|
+
return sha256(
|
|
70
|
+
stableJson(
|
|
71
|
+
SCANNER_PATHS.map((relative) => ({
|
|
72
|
+
path: relative,
|
|
73
|
+
digest: sha256(fs.readFileSync(path.join(runtimeRoot, relative))),
|
|
74
|
+
})),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveV4FloatingConsumerPolicyAuthority({
|
|
80
|
+
runtimeRoot = defaultRuntimeRoot(),
|
|
81
|
+
callerRoot = process.cwd(),
|
|
82
|
+
stableLockPath = DEFAULT_STABLE_LOCK_PATH,
|
|
83
|
+
alphaLockPath = DEFAULT_ALPHA_LOCK_PATH,
|
|
84
|
+
} = {}) {
|
|
85
|
+
const policy = readJson(
|
|
86
|
+
runtimeRoot,
|
|
87
|
+
"architecture/v4-floating-consumer-policy.json",
|
|
88
|
+
"v4 floating consumer policy",
|
|
89
|
+
).value;
|
|
90
|
+
const readLock = (relative, label) => {
|
|
91
|
+
const file = resolveInside(callerRoot, relative, `${label} contract lock`);
|
|
92
|
+
if (!fs.existsSync(file))
|
|
93
|
+
throw new Error(`${label} contract lock is missing: ${relative}`);
|
|
94
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
95
|
+
return { path: relative, root: v4FloatingConsumerDocumentRoot(value) };
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
policy,
|
|
99
|
+
policyRoot: v4FloatingConsumerDocumentRoot(policy),
|
|
100
|
+
scannerRoot: v4ConsumerPolicyScannerRoot(runtimeRoot),
|
|
101
|
+
contractLocks: {
|
|
102
|
+
stable: readLock(stableLockPath, "stable"),
|
|
103
|
+
alpha: readLock(alphaLockPath, "alpha"),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readJson(root, relative, label) {
|
|
109
|
+
const file = path.resolve(root, relative);
|
|
110
|
+
if (
|
|
111
|
+
!file.startsWith(`${path.resolve(root)}${path.sep}`) ||
|
|
112
|
+
!fs.existsSync(file)
|
|
113
|
+
) {
|
|
114
|
+
throw new Error(`${label} is missing: ${relative}`);
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
return { value: JSON.parse(fs.readFileSync(file, "utf8")), file };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`${label} is invalid JSON: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function listWorkflowFiles(root) {
|
|
124
|
+
const directory = path.join(root, ".github", "workflows");
|
|
125
|
+
if (!fs.existsSync(directory)) return [];
|
|
126
|
+
return fs
|
|
127
|
+
.readdirSync(directory, { withFileTypes: true })
|
|
128
|
+
.filter((entry) => entry.isFile() && /\.ya?ml$/u.test(entry.name))
|
|
129
|
+
.map((entry) => `.github/workflows/${entry.name}`)
|
|
130
|
+
.sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function coordinate(value) {
|
|
134
|
+
const match = String(value || "").match(/^([^/]+\/[^/]+)\/(.+)@([^@]+)$/u);
|
|
135
|
+
return match
|
|
136
|
+
? { repository: match[1], path: match[2], selector: match[3] }
|
|
137
|
+
: null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function localActionManifest(root, sourcePath, uses) {
|
|
141
|
+
if (!uses.startsWith("./")) return "";
|
|
142
|
+
if (TRANSIENT_ACTION.test(uses)) return "";
|
|
143
|
+
const target = path.resolve(root, uses);
|
|
144
|
+
if (!target.startsWith(`${path.resolve(root)}${path.sep}`)) return "";
|
|
145
|
+
const candidates =
|
|
146
|
+
fs.existsSync(target) && fs.statSync(target).isDirectory()
|
|
147
|
+
? [path.join(target, "action.yml"), path.join(target, "action.yaml")]
|
|
148
|
+
: [target];
|
|
149
|
+
const manifest = candidates.find((candidate) => fs.existsSync(candidate));
|
|
150
|
+
if (!manifest) {
|
|
151
|
+
throw new Error(`${sourcePath} references missing local action ${uses}`);
|
|
152
|
+
}
|
|
153
|
+
return path.relative(root, manifest).split(path.sep).join("/");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function enumerateUses(root) {
|
|
157
|
+
const queue = listWorkflowFiles(root);
|
|
158
|
+
const visited = new Set();
|
|
159
|
+
const records = [];
|
|
160
|
+
while (queue.length) {
|
|
161
|
+
const relative = queue.shift();
|
|
162
|
+
if (visited.has(relative)) continue;
|
|
163
|
+
visited.add(relative);
|
|
164
|
+
const text = fs.readFileSync(path.join(root, relative), "utf8");
|
|
165
|
+
for (const node of parseYamlUses(text)) {
|
|
166
|
+
records.push({
|
|
167
|
+
sourcePath: relative,
|
|
168
|
+
line: node.line,
|
|
169
|
+
scalarKind: node.scalar.kind,
|
|
170
|
+
uses: node.value,
|
|
171
|
+
});
|
|
172
|
+
const manifest = localActionManifest(root, relative, node.value);
|
|
173
|
+
if (manifest && !visited.has(manifest)) queue.push(manifest);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
files: [...visited].sort().map((relative) => ({
|
|
178
|
+
path: relative,
|
|
179
|
+
digest: sha256(fs.readFileSync(path.join(root, relative))),
|
|
180
|
+
})),
|
|
181
|
+
records,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function validateLock(lock, expectedRef, label, failures) {
|
|
186
|
+
if (lock?.contract !== "kungfu-buildchain-contract-lock") {
|
|
187
|
+
failures.push({
|
|
188
|
+
code: `${label}-contract-invalid`,
|
|
189
|
+
message: `${label} must be a Buildchain contract lock`,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (lock.buildchain?.ref !== expectedRef) {
|
|
194
|
+
failures.push({
|
|
195
|
+
code: `${label}-channel-mismatch`,
|
|
196
|
+
message: `${label} must bind ${expectedRef}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (lock.buildchain?.majorLine !== "v4") {
|
|
200
|
+
failures.push({
|
|
201
|
+
code: `${label}-major-mismatch`,
|
|
202
|
+
message: `${label} must bind major line v4`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
!EXACT_SHA.test(String(lock.buildchain?.resolvedSha || "").toLowerCase())
|
|
207
|
+
) {
|
|
208
|
+
failures.push({
|
|
209
|
+
code: `${label}-sha-invalid`,
|
|
210
|
+
message: `${label} resolvedSha must be an exact commit SHA`,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
for (const field of ["contractDigest", "compatibilityDigest"]) {
|
|
214
|
+
if (!SHA256_ROOT.test(String(lock.buildchain?.[field] || ""))) {
|
|
215
|
+
failures.push({
|
|
216
|
+
code: `${label}-${field}-invalid`,
|
|
217
|
+
message: `${label} ${field} must be a sha256 root`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function selectorFailure(record, selector) {
|
|
224
|
+
const location = `${record.sourcePath}:${record.line}`;
|
|
225
|
+
if (record.scalarKind === "expression" || selector.includes("${{")) {
|
|
226
|
+
return {
|
|
227
|
+
code: "persistent-selector-indirection",
|
|
228
|
+
message: `${location} uses repository/input/environment indirection for a persisted Buildchain selector`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (EXACT_SHA.test(selector.toLowerCase())) {
|
|
232
|
+
return {
|
|
233
|
+
code: "persistent-exact-sha-selector",
|
|
234
|
+
message: `${location} persists an exact Buildchain SHA; use v4 or v4-alpha plus contract locks`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
code: "unapproved-v4-selector",
|
|
239
|
+
message: `${location} uses ${selector || "<empty>"}; approved persisted selectors are v4 and v4-alpha`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeWorkflowPath(value) {
|
|
244
|
+
return `.github/workflows/${String(value || "").replace(/^\/+|^\.github\/workflows\//gu, "")}`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function validateScanIdentity(
|
|
248
|
+
{ repository, sourceSha, runtimeSha, workflowSha },
|
|
249
|
+
failures,
|
|
250
|
+
) {
|
|
251
|
+
for (const [value, code] of [
|
|
252
|
+
[sourceSha, "caller-source-sha-invalid"],
|
|
253
|
+
[runtimeSha, "resolved-runtime-sha-invalid"],
|
|
254
|
+
[workflowSha, "resolved-workflow-sha-invalid"],
|
|
255
|
+
])
|
|
256
|
+
if (!EXACT_SHA.test(value))
|
|
257
|
+
failures.push({ code, message: `${code} must bind an exact commit` });
|
|
258
|
+
if (!repository || !/^[^/]+\/[^/]+$/u.test(repository))
|
|
259
|
+
failures.push({
|
|
260
|
+
code: "caller-repository-invalid",
|
|
261
|
+
message: "caller repository must be owner/repo",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readContractLock(root, relative, expectedRef, label, failures) {
|
|
266
|
+
try {
|
|
267
|
+
const lock = readJson(root, relative, `${label} contract lock`).value;
|
|
268
|
+
validateLock(lock, expectedRef, `${label}-lock`, failures);
|
|
269
|
+
return lock;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
failures.push({ code: `${label}-lock-missing`, message: error.message });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function classifyBuildchainUses(records, failures) {
|
|
276
|
+
const uses = [];
|
|
277
|
+
for (const record of records) {
|
|
278
|
+
const parsed = coordinate(record.uses);
|
|
279
|
+
if (!parsed || parsed.repository !== BUILDCHAIN_REPOSITORY) continue;
|
|
280
|
+
const v4Candidate =
|
|
281
|
+
parsed.selector === "v4" ||
|
|
282
|
+
parsed.selector === "v4-alpha" ||
|
|
283
|
+
/^v4(?:[./-]|$)/u.test(parsed.selector) ||
|
|
284
|
+
EXACT_SHA.test(parsed.selector.toLowerCase()) ||
|
|
285
|
+
parsed.selector.includes("${{");
|
|
286
|
+
if (!v4Candidate) continue;
|
|
287
|
+
const channel = CHANNELS[parsed.selector] || "";
|
|
288
|
+
uses.push({
|
|
289
|
+
...record,
|
|
290
|
+
...parsed,
|
|
291
|
+
channel,
|
|
292
|
+
selectorClass: channel ? "floating" : "rejected",
|
|
293
|
+
});
|
|
294
|
+
if (!channel) failures.push(selectorFailure(record, parsed.selector));
|
|
295
|
+
}
|
|
296
|
+
return uses;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function selectInvocation(uses, workflow, failures, source = "", channel = "") {
|
|
300
|
+
const expectedWorkflow = normalizeWorkflowPath(workflow);
|
|
301
|
+
const invocations = uses.filter(
|
|
302
|
+
(entry) =>
|
|
303
|
+
normalizeWorkflowPath(entry.path) === expectedWorkflow &&
|
|
304
|
+
(!source || entry.sourcePath === source) &&
|
|
305
|
+
(!channel || entry.channel === channel),
|
|
306
|
+
);
|
|
307
|
+
if (invocations.length !== 1) {
|
|
308
|
+
failures.push({
|
|
309
|
+
code: invocations.length
|
|
310
|
+
? "invoked-workflow-ambiguous"
|
|
311
|
+
: "invoked-workflow-not-found",
|
|
312
|
+
message: `caller source must contain exactly one ${expectedWorkflow} Buildchain v4 invocation`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return { expectedWorkflow, selected: invocations[0] };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function scanV4FloatingConsumerPolicy({
|
|
319
|
+
root = process.cwd(),
|
|
320
|
+
repository = "",
|
|
321
|
+
sourceSha = "",
|
|
322
|
+
invokedWorkflow = "",
|
|
323
|
+
invocationSourcePath = "",
|
|
324
|
+
expectedInvocationChannel = "",
|
|
325
|
+
resolvedWorkflowSha = "",
|
|
326
|
+
resolvedRuntimeSha = "",
|
|
327
|
+
stableLockPath = ".buildchain/contract-lock.json",
|
|
328
|
+
alphaLockPath = ".buildchain/alpha-contract-lock.json",
|
|
329
|
+
policy,
|
|
330
|
+
scannerRoot = "",
|
|
331
|
+
} = {}) {
|
|
332
|
+
const resolvedRoot = path.resolve(root);
|
|
333
|
+
if (!policy || policy.contract !== V4_FLOATING_CONSUMER_POLICY) {
|
|
334
|
+
throw new Error(`policy must use ${V4_FLOATING_CONSUMER_POLICY}`);
|
|
335
|
+
}
|
|
336
|
+
const failures = [];
|
|
337
|
+
const normalizedSourceSha = String(sourceSha || "").toLowerCase();
|
|
338
|
+
const normalizedRuntimeSha = String(resolvedRuntimeSha || "").toLowerCase();
|
|
339
|
+
const normalizedWorkflowSha = String(
|
|
340
|
+
resolvedWorkflowSha || resolvedRuntimeSha || "",
|
|
341
|
+
).toLowerCase();
|
|
342
|
+
validateScanIdentity(
|
|
343
|
+
{
|
|
344
|
+
repository,
|
|
345
|
+
sourceSha: normalizedSourceSha,
|
|
346
|
+
runtimeSha: normalizedRuntimeSha,
|
|
347
|
+
workflowSha: normalizedWorkflowSha,
|
|
348
|
+
},
|
|
349
|
+
failures,
|
|
350
|
+
);
|
|
351
|
+
const stable = readContractLock(
|
|
352
|
+
resolvedRoot,
|
|
353
|
+
stableLockPath,
|
|
354
|
+
"v4",
|
|
355
|
+
"stable",
|
|
356
|
+
failures,
|
|
357
|
+
);
|
|
358
|
+
const alpha = readContractLock(
|
|
359
|
+
resolvedRoot,
|
|
360
|
+
alphaLockPath,
|
|
361
|
+
"v4-alpha",
|
|
362
|
+
"alpha",
|
|
363
|
+
failures,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
let scanned = { files: [], records: [] };
|
|
367
|
+
try {
|
|
368
|
+
scanned = enumerateUses(resolvedRoot);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
failures.push({ code: "source-scan-failed", message: error.message });
|
|
371
|
+
}
|
|
372
|
+
const buildchainUses = classifyBuildchainUses(scanned.records, failures);
|
|
373
|
+
const sourcePath = String(invocationSourcePath || "")
|
|
374
|
+
.split("@")[0]
|
|
375
|
+
.replace(`${repository}/`, "");
|
|
376
|
+
const { expectedWorkflow, selected } = selectInvocation(
|
|
377
|
+
buildchainUses,
|
|
378
|
+
invokedWorkflow,
|
|
379
|
+
failures,
|
|
380
|
+
sourcePath,
|
|
381
|
+
expectedInvocationChannel,
|
|
382
|
+
);
|
|
383
|
+
const selectedLock = selected?.channel === "alpha" ? alpha : stable;
|
|
384
|
+
if (
|
|
385
|
+
selected?.channel &&
|
|
386
|
+
selectedLock?.buildchain?.resolvedSha?.toLowerCase() !==
|
|
387
|
+
normalizedWorkflowSha
|
|
388
|
+
) {
|
|
389
|
+
failures.push({
|
|
390
|
+
code: "selected-lock-runtime-mismatch",
|
|
391
|
+
message: `${selected.channel} contract lock does not bind resolved workflow shell ${normalizedWorkflowSha}`,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const policyRoot = sha256(stableJson(policy));
|
|
396
|
+
const resolvedScannerRoot = scannerRoot || policyRoot;
|
|
397
|
+
if (!SHA256_ROOT.test(resolvedScannerRoot)) {
|
|
398
|
+
failures.push({
|
|
399
|
+
code: "scanner-root-invalid",
|
|
400
|
+
message: "scanner root must be a sha256 root",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const contractLockRoots = {
|
|
404
|
+
stable: stable ? sha256(stableJson(stable)) : "",
|
|
405
|
+
alpha: alpha ? sha256(stableJson(alpha)) : "",
|
|
406
|
+
};
|
|
407
|
+
const scanRoot = sha256(
|
|
408
|
+
stableJson({ files: scanned.files, invocations: buildchainUses }),
|
|
409
|
+
);
|
|
410
|
+
const receipt = {
|
|
411
|
+
schema: V4_FLOATING_CONSUMER_RECEIPT,
|
|
412
|
+
status: failures.length ? "failed" : "passed",
|
|
413
|
+
caller: { repository, sourceSha: normalizedSourceSha },
|
|
414
|
+
invocation: {
|
|
415
|
+
workflow: expectedWorkflow,
|
|
416
|
+
sourcePath: selected?.sourcePath || "",
|
|
417
|
+
sourceLine: selected?.line || 0,
|
|
418
|
+
visibleSelector: selected?.selector || "",
|
|
419
|
+
selectorClass: selected?.selectorClass || "",
|
|
420
|
+
channel: selected?.channel || "",
|
|
421
|
+
resolvedWorkflowSha: normalizedWorkflowSha,
|
|
422
|
+
resolvedRuntimeSha: normalizedRuntimeSha,
|
|
423
|
+
},
|
|
424
|
+
contractLocks: {
|
|
425
|
+
stable: { path: stableLockPath, root: contractLockRoots.stable },
|
|
426
|
+
alpha: { path: alphaLockPath, root: contractLockRoots.alpha },
|
|
427
|
+
},
|
|
428
|
+
policy: {
|
|
429
|
+
version: policy.policyVersion,
|
|
430
|
+
root: policyRoot,
|
|
431
|
+
scannerRoot: resolvedScannerRoot,
|
|
432
|
+
sourceScanRoot: scanRoot,
|
|
433
|
+
},
|
|
434
|
+
failures,
|
|
435
|
+
};
|
|
436
|
+
const receiptRoot = sha256(stableJson(receipt));
|
|
437
|
+
return {
|
|
438
|
+
schema: "kungfu-buildchain-v4-floating-consumer-policy-check/v1",
|
|
439
|
+
ok: failures.length === 0,
|
|
440
|
+
failures,
|
|
441
|
+
scannedFiles: scanned.files,
|
|
442
|
+
invocations: buildchainUses,
|
|
443
|
+
receipt,
|
|
444
|
+
receiptRoot,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
v4FloatingConsumerDocumentRoot,
|
|
5
|
+
verifyV4FloatingConsumerPolicyCertification,
|
|
6
|
+
} from "./v4-floating-consumer-evidence.js";
|
|
7
|
+
|
|
8
|
+
const SHA256_ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
9
|
+
|
|
10
|
+
export function isV4PromotionRouting(value = undefined) {
|
|
11
|
+
return [
|
|
12
|
+
value?.router?.ref,
|
|
13
|
+
value?.shell?.ref,
|
|
14
|
+
value?.runtime?.requestedRef,
|
|
15
|
+
].some((ref) => /^v4(?:-alpha)?$/u.test(String(ref || "")));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveInside(root, relative, label) {
|
|
19
|
+
const resolvedRoot = path.resolve(root);
|
|
20
|
+
const file = path.resolve(resolvedRoot, relative);
|
|
21
|
+
if (!file.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
22
|
+
throw new Error(`${label} escapes caller root: ${relative}`);
|
|
23
|
+
}
|
|
24
|
+
return file;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveCallerLock(root, relative, label) {
|
|
28
|
+
const file = resolveInside(root, relative, `${label} contract lock`);
|
|
29
|
+
if (!fs.existsSync(file)) {
|
|
30
|
+
throw new Error(`${label} contract lock is missing: ${relative}`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
path: relative,
|
|
34
|
+
root: v4FloatingConsumerDocumentRoot(
|
|
35
|
+
JSON.parse(fs.readFileSync(file, "utf8")),
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveV4FloatingConsumerCallerLocks(value, cwd) {
|
|
41
|
+
const certification = value?.certification || value;
|
|
42
|
+
const declared = certification?.authority || {};
|
|
43
|
+
const stableLockPath = declared?.contractLocks?.stable?.path || "";
|
|
44
|
+
const alphaLockPath = declared?.contractLocks?.alpha?.path || "";
|
|
45
|
+
return {
|
|
46
|
+
stableLockRoot: resolveCallerLock(cwd, stableLockPath, "stable").root,
|
|
47
|
+
alphaLockRoot: resolveCallerLock(cwd, alphaLockPath, "alpha").root,
|
|
48
|
+
stableLockPath,
|
|
49
|
+
alphaLockPath,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeCertification(value, expected) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
55
|
+
throw new Error("v4ConsumerPolicyCertification must be a JSON object");
|
|
56
|
+
}
|
|
57
|
+
const certification = value.certification || value;
|
|
58
|
+
const verification = verifyV4FloatingConsumerPolicyCertification({
|
|
59
|
+
certification,
|
|
60
|
+
certificationRoot: value.certificationRoot || "",
|
|
61
|
+
expectedCertificationRoot: expected.certificationRoot,
|
|
62
|
+
repository: expected.repository,
|
|
63
|
+
sourceSha: expected.sourceSha,
|
|
64
|
+
resolvedRuntimeSha: expected.runtimeSha,
|
|
65
|
+
policyRoot: expected.policyRoot,
|
|
66
|
+
scannerRoot: expected.scannerRoot,
|
|
67
|
+
stableLockRoot: expected.stableLockRoot,
|
|
68
|
+
alphaLockRoot: expected.alphaLockRoot,
|
|
69
|
+
stableLockPath: expected.stableLockPath,
|
|
70
|
+
alphaLockPath: expected.alphaLockPath,
|
|
71
|
+
});
|
|
72
|
+
if (!verification.ok) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`v4 consumer policy certification invalid: ${verification.failures.map((failure) => failure.code).join(", ")}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return { certificationRoot: verification.certificationRoot, certification };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function requireV4ConsumerPolicyCertification({
|
|
81
|
+
value,
|
|
82
|
+
repository,
|
|
83
|
+
sourceSha,
|
|
84
|
+
routing,
|
|
85
|
+
cwd,
|
|
86
|
+
certificationRoot,
|
|
87
|
+
}) {
|
|
88
|
+
const v4Routing = isV4PromotionRouting(routing);
|
|
89
|
+
if (v4Routing && !value) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"Buildchain v4 Release Passport requires an external floating consumer policy certification",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!value) return undefined;
|
|
95
|
+
if (v4Routing && !SHA256_ROOT.test(String(certificationRoot || ""))) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Buildchain v4 Release Passport requires an expected external certification root",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return normalizeCertification(value, {
|
|
101
|
+
repository,
|
|
102
|
+
sourceSha,
|
|
103
|
+
runtimeSha: routing?.runtime?.resolvedSha || "",
|
|
104
|
+
certificationRoot,
|
|
105
|
+
...(v4Routing ? resolveV4FloatingConsumerCallerLocks(value, cwd) : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function releasePassportCertificationVerificationOptions({
|
|
110
|
+
evidence,
|
|
111
|
+
passport,
|
|
112
|
+
routing,
|
|
113
|
+
}) {
|
|
114
|
+
const authority = evidence?.certification?.authority || {};
|
|
115
|
+
return {
|
|
116
|
+
certification: evidence?.certification,
|
|
117
|
+
certificationRoot: evidence?.certificationRoot,
|
|
118
|
+
repository: passport?.product?.repository || "",
|
|
119
|
+
sourceSha: passport?.release?.sourceSha || "",
|
|
120
|
+
resolvedRuntimeSha: routing?.runtime?.resolvedSha || "",
|
|
121
|
+
stableLockRoot: authority.contractLocks?.stable?.root || "",
|
|
122
|
+
alphaLockRoot: authority.contractLocks?.alpha?.root || "",
|
|
123
|
+
stableLockPath: authority.contractLocks?.stable?.path || "",
|
|
124
|
+
alphaLockPath: authority.contractLocks?.alpha?.path || "",
|
|
125
|
+
};
|
|
126
|
+
}
|