@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/report-buildchain-issue/README.md +1 -1
- package/contracts/buildchain-v2-residuals-v1.json +191 -0
- package/dist/site/buildchain-contract.json +58 -35
- package/dist/site/buildchain-site.json +33 -33
- package/dist/site/controller-registry.json +24 -3
- package/dist/site/kfd-claims.json +11 -6
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +6 -6
- package/dist/site/page-registry.json +22 -22
- package/dist/site/public-surface-audit.json +10 -5
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +8 -3
- package/docs/auditable-demo.md +19 -1
- package/docs/consumer-issue-reporting.md +1 -1
- package/docs/dev-alpha-candidate-patrol.md +24 -10
- package/docs/github-governance-authority.md +23 -30
- package/docs/observed-evidence-patrol.md +28 -12
- package/docs/release-governance.md +8 -1
- package/docs/reusable-build-surface.md +124 -64
- package/docs/shifu-gate-profiles.md +7 -1
- package/docs/versioning.md +41 -21
- package/docs/web-surface-deployments.md +20 -1
- package/package.json +1 -1
- package/packages/core/artifact-signing.js +1 -0
- package/packages/core/buildchain-contract.js +1 -0
- package/packages/core/controller-evidence.js +2 -0
- package/packages/core/github-governance-authority.js +25 -17
- package/packages/core/publication-control-plane-audit.js +28 -0
- package/packages/core/release-passport.js +36 -27
- package/packages/core/stable-release-gate.js +4 -1
- package/scripts/artifact-signing-delegation.mjs +268 -0
- package/scripts/audit-github-governance.mjs +32 -13
- package/scripts/audit-publication-control-plane.mjs +17 -8
- package/scripts/auditable-demo.mjs +147 -2
- package/scripts/buildchain-patrol.mjs +1 -1
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +20 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
- package/scripts/finalize-native-artifact-signing-result.mjs +114 -31
- package/scripts/gate-profile-core.mjs +6 -1
- package/scripts/inspect-artifact-signing-requests.mjs +45 -14
- package/scripts/observed-evidence.mjs +151 -33
- package/scripts/reconcile-github-governance.mjs +8 -1
- package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
- package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
- package/scripts/runtime-ref-core.mjs +13 -2
- package/scripts/seal-artifact-signing-requests.mjs +51 -6
- package/scripts/stable-candidate-qualification.mjs +8 -0
- package/scripts/verify-artifact-signing-results.mjs +26 -1
- package/scripts/web-surface-production-decision.mjs +19 -3
|
@@ -22,6 +22,19 @@ const REQUIRED_ADAPTER_FILES = [
|
|
|
22
22
|
"public-projection.json",
|
|
23
23
|
"scene.json",
|
|
24
24
|
];
|
|
25
|
+
const OPTIONAL_ADAPTER_FILES = ["terminal-capture.json"];
|
|
26
|
+
const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
27
|
+
const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
|
|
28
|
+
const TERMINAL_CAPTURE_NON_AUTHORITIES = [
|
|
29
|
+
"first-party-identity",
|
|
30
|
+
"system-identity",
|
|
31
|
+
"kfd-compliance",
|
|
32
|
+
"product-system-metadata",
|
|
33
|
+
"package-metadata",
|
|
34
|
+
"registry-history",
|
|
35
|
+
"scan-output",
|
|
36
|
+
"standalone-generation",
|
|
37
|
+
];
|
|
25
38
|
|
|
26
39
|
function invariant(condition, message) {
|
|
27
40
|
if (!condition) throw new Error(message);
|
|
@@ -155,6 +168,19 @@ function text(value, minimum, maximum, label) {
|
|
|
155
168
|
return value;
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
function decodeBase64(value, label) {
|
|
172
|
+
invariant(
|
|
173
|
+
typeof value === "string"
|
|
174
|
+
&& value.length > 0
|
|
175
|
+
&& value.length <= MAX_TERMINAL_CAPTURE_BYTES * 2
|
|
176
|
+
&& /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value),
|
|
177
|
+
`${label} must be canonical base64`,
|
|
178
|
+
);
|
|
179
|
+
const decoded = Buffer.from(value, "base64");
|
|
180
|
+
invariant(decoded.toString("base64") === value, `${label} must be canonical base64`);
|
|
181
|
+
return decoded;
|
|
182
|
+
}
|
|
183
|
+
|
|
158
184
|
function validateScene(value) {
|
|
159
185
|
exactKeys(
|
|
160
186
|
value,
|
|
@@ -223,6 +249,81 @@ function validateProjection(value, scene, transcriptLineCount) {
|
|
|
223
249
|
};
|
|
224
250
|
}
|
|
225
251
|
|
|
252
|
+
function validateTerminalCapture(value, scene) {
|
|
253
|
+
exactKeys(
|
|
254
|
+
value,
|
|
255
|
+
[
|
|
256
|
+
"schema",
|
|
257
|
+
"command",
|
|
258
|
+
"dimensions",
|
|
259
|
+
"durationMs",
|
|
260
|
+
"encoding",
|
|
261
|
+
"events",
|
|
262
|
+
"completion",
|
|
263
|
+
"exitCode",
|
|
264
|
+
"authority",
|
|
265
|
+
],
|
|
266
|
+
[],
|
|
267
|
+
"terminalCapture",
|
|
268
|
+
);
|
|
269
|
+
invariant(value.schema === "kungfu.terminal-capture/v1", "unsupported terminal capture schema");
|
|
270
|
+
text(value.command, 1, 160, "terminalCapture.command");
|
|
271
|
+
exactKeys(value.dimensions, ["columns", "rows"], [], "terminalCapture.dimensions");
|
|
272
|
+
integer(value.dimensions.columns, 80, 200, "terminalCapture.dimensions.columns");
|
|
273
|
+
integer(value.dimensions.rows, 24, 80, "terminalCapture.dimensions.rows");
|
|
274
|
+
const durationMs = integer(value.durationMs, 500, 60000, "terminalCapture.durationMs");
|
|
275
|
+
invariant(
|
|
276
|
+
durationMs <= scene.durationMs && scene.durationMs - durationMs <= 2000,
|
|
277
|
+
"terminal capture duration must end within two seconds of the scene",
|
|
278
|
+
);
|
|
279
|
+
invariant(value.encoding === "base64", "terminalCapture.encoding must be base64");
|
|
280
|
+
invariant(
|
|
281
|
+
Array.isArray(value.events)
|
|
282
|
+
&& value.events.length > 0
|
|
283
|
+
&& value.events.length <= MAX_TERMINAL_CAPTURE_EVENTS,
|
|
284
|
+
`terminalCapture.events must contain 1 through ${MAX_TERMINAL_CAPTURE_EVENTS} events`,
|
|
285
|
+
);
|
|
286
|
+
let previousAtMs = -1;
|
|
287
|
+
let totalBytes = 0;
|
|
288
|
+
for (const [index, event] of value.events.entries()) {
|
|
289
|
+
exactKeys(event, ["atMs", "data"], [], `terminalCapture.events[${index}]`);
|
|
290
|
+
const atMs = integer(event.atMs, 0, durationMs - 1, `terminalCapture.events[${index}].atMs`);
|
|
291
|
+
invariant(atMs >= previousAtMs, "terminal capture event timestamps must be monotonic");
|
|
292
|
+
invariant(index > 0 || atMs === 0, "the first terminal capture event must start at zero");
|
|
293
|
+
previousAtMs = atMs;
|
|
294
|
+
totalBytes += decodeBase64(event.data, `terminalCapture.events[${index}].data`).length;
|
|
295
|
+
invariant(totalBytes <= MAX_TERMINAL_CAPTURE_BYTES, "terminal capture exceeds the 4 MiB byte bound");
|
|
296
|
+
}
|
|
297
|
+
exactKeys(
|
|
298
|
+
value.completion,
|
|
299
|
+
["schema", "status", "reportRoot", "eventCount"],
|
|
300
|
+
[],
|
|
301
|
+
"terminalCapture.completion",
|
|
302
|
+
);
|
|
303
|
+
invariant(
|
|
304
|
+
value.completion.schema === "kungfu.agent-work-lab.tui-autoplay/v1"
|
|
305
|
+
&& value.completion.status === "qualified"
|
|
306
|
+
&& DIGEST_PATTERN.test(value.completion.reportRoot),
|
|
307
|
+
"terminal capture completion sentinel is not a qualified Agent Work Lab autoplay",
|
|
308
|
+
);
|
|
309
|
+
integer(value.completion.eventCount, 1, 100_000, "terminalCapture.completion.eventCount");
|
|
310
|
+
invariant(value.exitCode === 0, "terminal capture exitCode must be zero");
|
|
311
|
+
exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "terminalCapture.authority");
|
|
312
|
+
invariant(
|
|
313
|
+
value.authority.classification === "volatile-terminal-observation",
|
|
314
|
+
"terminal capture authority classification must remain observation-only",
|
|
315
|
+
);
|
|
316
|
+
invariant(
|
|
317
|
+
Array.isArray(value.authority.grants) && value.authority.grants.length === 0,
|
|
318
|
+
"terminal capture must not grant authority",
|
|
319
|
+
);
|
|
320
|
+
invariant(
|
|
321
|
+
JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(TERMINAL_CAPTURE_NON_AUTHORITIES),
|
|
322
|
+
"terminal capture must declare every identity and metadata non-authority",
|
|
323
|
+
);
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
|
|
226
327
|
function validateSourceCoordinate(value) {
|
|
227
328
|
exactKeys(
|
|
228
329
|
value,
|
|
@@ -276,11 +377,21 @@ function validateAdapterOutput(output, strict = true) {
|
|
|
276
377
|
scene,
|
|
277
378
|
lines.length,
|
|
278
379
|
);
|
|
380
|
+
const terminalCapturePath = path.join(output, "terminal-capture.json");
|
|
381
|
+
const terminalCapture = fs.existsSync(terminalCapturePath)
|
|
382
|
+
? validateTerminalCapture(readJson(terminalCapturePath, "terminal capture"), scene)
|
|
383
|
+
: null;
|
|
279
384
|
if (strict) {
|
|
280
|
-
const allowed = new Set(REQUIRED_ADAPTER_FILES);
|
|
385
|
+
const allowed = new Set([...REQUIRED_ADAPTER_FILES, ...OPTIONAL_ADAPTER_FILES]);
|
|
281
386
|
for (const member of listFiles(output)) invariant(allowed.has(member), `undeclared adapter output: ${member}`);
|
|
282
387
|
}
|
|
283
|
-
return {
|
|
388
|
+
return {
|
|
389
|
+
transcript: transcript.endsWith("\n") ? transcript : `${transcript}\n`,
|
|
390
|
+
lines,
|
|
391
|
+
scene,
|
|
392
|
+
projection,
|
|
393
|
+
terminalCapture,
|
|
394
|
+
};
|
|
284
395
|
}
|
|
285
396
|
|
|
286
397
|
function parseArguments(argv) {
|
|
@@ -355,6 +466,9 @@ function runAdapter(values) {
|
|
|
355
466
|
fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
|
|
356
467
|
writeJson(path.join(output, "scene.json"), normalized.scene);
|
|
357
468
|
writeJson(path.join(output, "public-projection.json"), normalized.projection);
|
|
469
|
+
if (normalized.terminalCapture) {
|
|
470
|
+
writeJson(path.join(output, "terminal-capture.json"), normalized.terminalCapture);
|
|
471
|
+
}
|
|
358
472
|
writeJson(path.join(diagnostics, "adapter.json"), {
|
|
359
473
|
schema: "buildchain.auditable-demo-adapter-execution/v1",
|
|
360
474
|
path: adapterRelative,
|
|
@@ -885,6 +999,12 @@ function finalizeGate(values) {
|
|
|
885
999
|
fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
|
|
886
1000
|
writeJson(path.join(output, "scene.json"), normalized.scene);
|
|
887
1001
|
writeJson(path.join(output, "public-projection.json"), normalized.projection);
|
|
1002
|
+
if (normalized.terminalCapture) {
|
|
1003
|
+
copyFile(
|
|
1004
|
+
path.join(adapterOutput, "terminal-capture.json"),
|
|
1005
|
+
path.join(output, "terminal-capture.json"),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
888
1008
|
copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
|
|
889
1009
|
copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
|
|
890
1010
|
for (const name of listFiles(smokeOutput)) {
|
|
@@ -914,6 +1034,14 @@ function finalizeGate(values) {
|
|
|
914
1034
|
transcript: sha256(readRegular(path.join(output, "complete-transcript.txt"), "transcript")),
|
|
915
1035
|
projection: sha256(readRegular(path.join(output, "public-projection.json"), "projection")),
|
|
916
1036
|
scene: sha256(readRegular(path.join(output, "scene.json"), "scene")),
|
|
1037
|
+
...(normalized.terminalCapture
|
|
1038
|
+
? {
|
|
1039
|
+
terminalCapture: {
|
|
1040
|
+
schema: normalized.terminalCapture.schema,
|
|
1041
|
+
root: sha256(readRegular(path.join(output, "terminal-capture.json"), "terminal capture")),
|
|
1042
|
+
},
|
|
1043
|
+
}
|
|
1044
|
+
: {}),
|
|
917
1045
|
evidenceClass: normalized.projection.evidenceClass,
|
|
918
1046
|
claimBoundary: normalized.projection.claimBoundary,
|
|
919
1047
|
},
|
|
@@ -945,6 +1073,19 @@ function verifyGate(values) {
|
|
|
945
1073
|
&& receipt.qualifiedInputs?.scene === sha256(readRegular(path.join(bundle, "scene.json"), "scene")),
|
|
946
1074
|
"gate qualified input roots mismatch",
|
|
947
1075
|
);
|
|
1076
|
+
const qualifiedCapture = receipt.qualifiedInputs?.terminalCapture;
|
|
1077
|
+
invariant(
|
|
1078
|
+
Boolean(qualifiedCapture) === Boolean(normalized.terminalCapture),
|
|
1079
|
+
"gate terminal capture presence drifted",
|
|
1080
|
+
);
|
|
1081
|
+
if (normalized.terminalCapture) {
|
|
1082
|
+
invariant(
|
|
1083
|
+
qualifiedCapture.schema === normalized.terminalCapture.schema
|
|
1084
|
+
&& qualifiedCapture.root
|
|
1085
|
+
=== sha256(readRegular(path.join(bundle, "terminal-capture.json"), "terminal capture")),
|
|
1086
|
+
"gate terminal capture root mismatch",
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
948
1089
|
invariant(receipt.qualifiedInputs.evidenceClass === normalized.projection.evidenceClass, "gate evidence class drifted");
|
|
949
1090
|
}
|
|
950
1091
|
|
|
@@ -973,10 +1114,14 @@ function finalizeMedia(values) {
|
|
|
973
1114
|
"--renderer-image": rendererImage,
|
|
974
1115
|
"--source-sha": sourceSha,
|
|
975
1116
|
});
|
|
1117
|
+
const terminalCapturePath = path.join(gateBundle, "terminal-capture.json");
|
|
976
1118
|
const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
|
|
977
1119
|
scene: path.join(gateBundle, "scene.json"),
|
|
978
1120
|
transcript: path.join(gateBundle, "complete-transcript.txt"),
|
|
979
1121
|
projection: path.join(gateBundle, "public-projection.json"),
|
|
1122
|
+
...(fs.existsSync(terminalCapturePath)
|
|
1123
|
+
? { terminalCapture: terminalCapturePath }
|
|
1124
|
+
: {}),
|
|
980
1125
|
}, {
|
|
981
1126
|
mediaProfile,
|
|
982
1127
|
inspectMedia: mediaInspection?.inspectMedia,
|
|
@@ -49,7 +49,7 @@ function normalizeRepository(value) {
|
|
|
49
49
|
function normalizeTargetBranch(value) {
|
|
50
50
|
const branch = String(value || process.env.GITHUB_REF_NAME || DEFAULT_TARGET_BRANCH).replace(/^refs\/heads\//, "");
|
|
51
51
|
if (!/^dev\/v\d+\/v\d+\.\d+$/.test(branch)) {
|
|
52
|
-
throw new Error(`target-branch must be a semver dev branch such as dev/
|
|
52
|
+
throw new Error(`target-branch must be a semver dev branch such as dev/v3/v3.N, got: ${branch || "<empty>"}`);
|
|
53
53
|
}
|
|
54
54
|
return branch;
|
|
55
55
|
}
|
|
@@ -73,6 +73,7 @@ const requiredPaths = [
|
|
|
73
73
|
"docs/auditable-demo.md",
|
|
74
74
|
"contracts/auditable-demo-media-profiles-v1.json",
|
|
75
75
|
"contracts/evidence/auditable-demo-web-delivery-v1.json",
|
|
76
|
+
"contracts/buildchain-v2-residuals-v1.json",
|
|
76
77
|
"contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt",
|
|
77
78
|
"contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json",
|
|
78
79
|
"contracts/fixtures/auditable-demo-web-delivery-v1/scene.json",
|
|
@@ -65,6 +65,13 @@ function pullRequestBodyPrefix(value) {
|
|
|
65
65
|
return normalized;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function optionalExactSha(value, name) {
|
|
69
|
+
const normalized = text(value);
|
|
70
|
+
if (normalized && !EXACT_SHA.test(normalized))
|
|
71
|
+
throw new Error(`${name} must be an exact 40-character commit SHA`);
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
function canonical(value) {
|
|
69
76
|
if (Array.isArray(value)) return value.map(canonical);
|
|
70
77
|
if (value && typeof value === "object") {
|
|
@@ -225,6 +232,11 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
|
|
|
225
232
|
options.pullRequestBodyPrefix ??
|
|
226
233
|
process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX,
|
|
227
234
|
),
|
|
235
|
+
expectedSelectedSha: optionalExactSha(
|
|
236
|
+
options.expectedSelectedSha ??
|
|
237
|
+
process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_SELECTED_SHA,
|
|
238
|
+
"expectedSelectedSha",
|
|
239
|
+
),
|
|
228
240
|
createPullRequest,
|
|
229
241
|
settlementAuthorized: bool(
|
|
230
242
|
options.settlementAuthorized ??
|
|
@@ -526,6 +538,14 @@ export async function runDevAlphaCandidatePatrol(
|
|
|
526
538
|
maxAgeSeconds: options.maxAgeSeconds,
|
|
527
539
|
now: options.now,
|
|
528
540
|
});
|
|
541
|
+
if (
|
|
542
|
+
options.expectedSelectedSha &&
|
|
543
|
+
decision.source.sha !== options.expectedSelectedSha
|
|
544
|
+
) {
|
|
545
|
+
throw new Error(
|
|
546
|
+
`selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
529
549
|
const openPullRequests = await client.listOpenPullRequests(
|
|
530
550
|
options.targetBranch,
|
|
531
551
|
);
|
|
@@ -17,6 +17,12 @@ function repository(value, label) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const TRANSIENT_GITHUB_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
20
|
+
const DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF = "authority/v3/v3.0/artifact-signing";
|
|
21
|
+
|
|
22
|
+
export function resolveAuthorityDispatchRef(value) {
|
|
23
|
+
const ref = required(value, "authority ref");
|
|
24
|
+
return /^[0-9a-f]{40}$/u.test(ref) ? DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF : ref;
|
|
25
|
+
}
|
|
20
26
|
|
|
21
27
|
function retryDelayMs(attempt) {
|
|
22
28
|
return Math.min(1_000 * (2 ** (attempt - 1)), 10_000);
|
|
@@ -98,7 +104,7 @@ export async function dispatchArtifactSigningAuthority({
|
|
|
98
104
|
const authToken = required(token, "Buildchain authority dispatch token");
|
|
99
105
|
const authorityRepo = repository(authorityRepository, "authority repository");
|
|
100
106
|
const sourceRepo = repository(sourceRepository, "source repository");
|
|
101
|
-
const ref =
|
|
107
|
+
const ref = resolveAuthorityDispatchRef(authorityRef);
|
|
102
108
|
const runtime = required(runtimeSha, "Buildchain runtime SHA");
|
|
103
109
|
if (!/^[0-9a-f]{40}$/u.test(runtime)) throw new Error("Buildchain runtime SHA must be exact");
|
|
104
110
|
const correlationId = `${required(sourceRunId, "source run ID")}-${required(sourceRunAttempt, "source run attempt")}-${runtime.slice(0, 12)}-${required(requestArtifact, "request artifact").replace(/[^A-Za-z0-9._-]+/gu, "-")}`;
|
|
@@ -4,8 +4,14 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
createArtifactSigningReceipt,
|
|
9
|
+
validateArtifactSigningRequest,
|
|
10
|
+
} from "../packages/core/artifact-signing.js";
|
|
11
|
+
import {
|
|
12
|
+
artifactSigningEvidenceDigest,
|
|
13
|
+
createArtifactSigningResult,
|
|
14
|
+
} from "../packages/core/artifact-signing-result.js";
|
|
9
15
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
10
16
|
|
|
11
17
|
function required(value, label) {
|
|
@@ -21,7 +27,8 @@ function sha256File(filePath) {
|
|
|
21
27
|
function resolveBelow(root, relative, label) {
|
|
22
28
|
const target = path.resolve(root, required(relative, label));
|
|
23
29
|
const rel = path.relative(path.resolve(root), target);
|
|
24
|
-
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
30
|
+
if (rel.startsWith("..") || path.isAbsolute(rel))
|
|
31
|
+
throw new Error(`${label} escapes its root`);
|
|
25
32
|
return target;
|
|
26
33
|
}
|
|
27
34
|
|
|
@@ -34,63 +41,139 @@ export function finalizeNativeArtifactSigningResult({
|
|
|
34
41
|
checks = process.env.BUILDCHAIN_SIGNING_VERIFICATION_CHECKS,
|
|
35
42
|
} = {}) {
|
|
36
43
|
const requests = path.resolve(required(requestRoot, "signing request root"));
|
|
37
|
-
const request = JSON.parse(
|
|
44
|
+
const request = JSON.parse(
|
|
45
|
+
fs.readFileSync(
|
|
46
|
+
resolveBelow(requests, requestPath, "request path"),
|
|
47
|
+
"utf8",
|
|
48
|
+
),
|
|
49
|
+
);
|
|
38
50
|
const requestCheck = validateArtifactSigningRequest(request);
|
|
39
|
-
if (!requestCheck.ok)
|
|
51
|
+
if (!requestCheck.ok)
|
|
52
|
+
throw new Error(
|
|
53
|
+
`invalid signing request: ${requestCheck.issues.join(", ")}`,
|
|
54
|
+
);
|
|
40
55
|
if (request.signature.semantics !== "native-platform-signature") {
|
|
41
|
-
throw new Error(
|
|
56
|
+
throw new Error(
|
|
57
|
+
"native result finalizer rejects non-native signature profiles",
|
|
58
|
+
);
|
|
42
59
|
}
|
|
43
60
|
const payloadSource = path.resolve(required(signedPayload, "signed payload"));
|
|
44
|
-
const evidenceSource = path.resolve(
|
|
45
|
-
|
|
61
|
+
const evidenceSource = path.resolve(
|
|
62
|
+
required(evidencePath, "signing evidence"),
|
|
63
|
+
);
|
|
64
|
+
const resultDirectory = path.resolve(
|
|
65
|
+
required(outputRoot, "signing result root"),
|
|
66
|
+
);
|
|
46
67
|
fs.mkdirSync(path.join(resultDirectory, "payload"), { recursive: true });
|
|
47
|
-
const payloadPath = path.join(
|
|
68
|
+
const payloadPath = path.join(
|
|
69
|
+
resultDirectory,
|
|
70
|
+
"payload",
|
|
71
|
+
path.basename(payloadSource),
|
|
72
|
+
);
|
|
48
73
|
fs.copyFileSync(payloadSource, payloadPath, fs.constants.COPYFILE_EXCL);
|
|
49
74
|
const evidenceOutput = path.join(resultDirectory, "provider-evidence.json");
|
|
50
75
|
fs.copyFileSync(evidenceSource, evidenceOutput, fs.constants.COPYFILE_EXCL);
|
|
51
76
|
const evidenceDocument = JSON.parse(fs.readFileSync(evidenceOutput, "utf8"));
|
|
52
|
-
if (
|
|
53
|
-
|
|
77
|
+
if (
|
|
78
|
+
evidenceDocument.status !== "passed" ||
|
|
79
|
+
evidenceDocument.provider !== request.signature.provider
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"provider evidence does not prove the requested native signature",
|
|
83
|
+
);
|
|
54
84
|
}
|
|
55
|
-
const evidence = [
|
|
85
|
+
const evidence = [
|
|
86
|
+
{
|
|
87
|
+
kind: `${request.signature.profile}-verification`,
|
|
88
|
+
path: "provider-evidence.json",
|
|
89
|
+
digest: sha256File(evidenceOutput),
|
|
90
|
+
},
|
|
91
|
+
];
|
|
56
92
|
const payloadDigest = sha256File(payloadPath);
|
|
57
93
|
const receipt = createArtifactSigningReceipt({
|
|
58
94
|
request,
|
|
59
|
-
result: {
|
|
60
|
-
|
|
95
|
+
result: {
|
|
96
|
+
artifactDigest: payloadDigest,
|
|
97
|
+
evidenceDigest: artifactSigningEvidenceDigest(evidence),
|
|
98
|
+
},
|
|
99
|
+
signatures: [
|
|
100
|
+
{ kind: request.signature.profile, digest: evidence[0].digest },
|
|
101
|
+
],
|
|
61
102
|
});
|
|
62
|
-
fs.writeFileSync(
|
|
63
|
-
|
|
103
|
+
fs.writeFileSync(
|
|
104
|
+
path.join(resultDirectory, "receipt.json"),
|
|
105
|
+
`${JSON.stringify(receipt, null, 2)}\n`,
|
|
106
|
+
);
|
|
107
|
+
const verificationChecks = String(checks || "")
|
|
108
|
+
.split(",")
|
|
109
|
+
.map((value) => value.trim())
|
|
110
|
+
.filter(Boolean);
|
|
111
|
+
if (
|
|
112
|
+
verificationChecks.length === 0 &&
|
|
113
|
+
Array.isArray(evidenceDocument.checks)
|
|
114
|
+
) {
|
|
115
|
+
verificationChecks.push(
|
|
116
|
+
...evidenceDocument.checks
|
|
117
|
+
.map((value) => String(value).trim())
|
|
118
|
+
.filter(Boolean),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (verificationChecks.length === 0)
|
|
122
|
+
throw new Error("provider evidence contains no verification checks");
|
|
64
123
|
const result = createArtifactSigningResult({
|
|
65
124
|
request,
|
|
66
125
|
receipt,
|
|
67
|
-
payload: {
|
|
126
|
+
payload: {
|
|
127
|
+
path: `payload/${path.basename(payloadPath)}`,
|
|
128
|
+
bytes: fs.statSync(payloadPath).size,
|
|
129
|
+
digest: payloadDigest,
|
|
130
|
+
},
|
|
68
131
|
evidence,
|
|
69
|
-
verification: {
|
|
132
|
+
verification: {
|
|
133
|
+
status: "passed",
|
|
134
|
+
provider: request.signature.provider,
|
|
135
|
+
checks: verificationChecks,
|
|
136
|
+
},
|
|
70
137
|
});
|
|
71
|
-
fs.writeFileSync(
|
|
138
|
+
fs.writeFileSync(
|
|
139
|
+
path.join(resultDirectory, "result.json"),
|
|
140
|
+
`${JSON.stringify(result, null, 2)}\n`,
|
|
141
|
+
);
|
|
72
142
|
const index = {
|
|
73
143
|
schemaVersion: 1,
|
|
74
144
|
contract: "kungfu-buildchain-artifact-signing-result-index/v1",
|
|
75
|
-
results: [
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
145
|
+
results: [
|
|
146
|
+
{
|
|
147
|
+
id: request.artifact.id,
|
|
148
|
+
requestDigest: request.digest,
|
|
149
|
+
resultDigest: result.digest,
|
|
150
|
+
result: "result.json",
|
|
151
|
+
payload: result.artifact.path,
|
|
152
|
+
receipt: "receipt.json",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
83
155
|
};
|
|
84
|
-
fs.writeFileSync(
|
|
85
|
-
|
|
156
|
+
fs.writeFileSync(
|
|
157
|
+
path.join(resultDirectory, "index.json"),
|
|
158
|
+
`${JSON.stringify(index, null, 2)}\n`,
|
|
159
|
+
);
|
|
160
|
+
writeGitHubOutputs({
|
|
161
|
+
"result-root": resultDirectory,
|
|
162
|
+
"result-index": path.join(resultDirectory, "index.json"),
|
|
163
|
+
});
|
|
86
164
|
return index;
|
|
87
165
|
}
|
|
88
166
|
|
|
89
|
-
if (
|
|
167
|
+
if (
|
|
168
|
+
process.argv[1] &&
|
|
169
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
170
|
+
) {
|
|
90
171
|
try {
|
|
91
172
|
finalizeNativeArtifactSigningResult();
|
|
92
173
|
} catch (error) {
|
|
93
|
-
console.error(
|
|
174
|
+
console.error(
|
|
175
|
+
`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
|
|
176
|
+
);
|
|
94
177
|
process.exitCode = 1;
|
|
95
178
|
}
|
|
96
179
|
}
|
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
export const GATE_MATRIX_CONTRACT = "buildchain.shifu-gate-matrix/v1";
|
|
4
4
|
export const GATE_AGGREGATE_CONTRACT = "buildchain.shifu-gate-aggregate/v1";
|
|
5
|
+
export const GATE_JOB_CONTROL_PLANE_OVERHEAD_MINUTES = 30;
|
|
5
6
|
|
|
6
7
|
function stableValue(value) {
|
|
7
8
|
if (Array.isArray(value)) return value.map(stableValue);
|
|
@@ -265,7 +266,11 @@ export function createGateExecutionMatrix({
|
|
|
265
266
|
planDigest: inspected.planDigest,
|
|
266
267
|
timeoutMinutes: Math.max(
|
|
267
268
|
1,
|
|
268
|
-
Math.min(
|
|
269
|
+
Math.min(
|
|
270
|
+
360,
|
|
271
|
+
Math.ceil(timeoutSeconds / 60) +
|
|
272
|
+
GATE_JOB_CONTROL_PLANE_OVERHEAD_MINUTES,
|
|
273
|
+
),
|
|
269
274
|
),
|
|
270
275
|
gates: inspected.gates.map((gate) => ({
|
|
271
276
|
id: gate.id,
|
|
@@ -38,7 +38,8 @@ export function inspectArtifactSigningRequests({
|
|
|
38
38
|
} = {}) {
|
|
39
39
|
const root = path.resolve(required(inputRoot, "signing request root"));
|
|
40
40
|
const indexes = walk(root, "index.json");
|
|
41
|
-
if (indexes.length === 0)
|
|
41
|
+
if (indexes.length === 0)
|
|
42
|
+
throw new Error("no artifact signing request indexes found");
|
|
42
43
|
const seen = new Set();
|
|
43
44
|
const matrices = {
|
|
44
45
|
detached: [],
|
|
@@ -47,41 +48,66 @@ export function inspectArtifactSigningRequests({
|
|
|
47
48
|
};
|
|
48
49
|
for (const indexPath of indexes) {
|
|
49
50
|
const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
|
|
50
|
-
if (
|
|
51
|
+
if (
|
|
52
|
+
index.contract !== "kungfu-buildchain-artifact-signing-request-index/v1"
|
|
53
|
+
)
|
|
54
|
+
continue;
|
|
51
55
|
for (const entry of index.requests || []) {
|
|
52
56
|
const requestPath = path.resolve(path.dirname(indexPath), entry.path);
|
|
53
57
|
const relative = path.relative(root, requestPath);
|
|
54
58
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
55
|
-
throw new Error(
|
|
59
|
+
throw new Error(
|
|
60
|
+
"signing request path escapes the authority intake root",
|
|
61
|
+
);
|
|
56
62
|
}
|
|
57
63
|
const request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
|
|
58
64
|
const check = validateArtifactSigningRequest(request);
|
|
59
65
|
if (!check.ok || request.digest !== entry.digest) {
|
|
60
66
|
throw new Error(`invalid artifact signing request: ${entry.id}`);
|
|
61
67
|
}
|
|
62
|
-
if (
|
|
68
|
+
if (
|
|
69
|
+
expectedRepository &&
|
|
70
|
+
request.source.repository !== expectedRepository
|
|
71
|
+
) {
|
|
63
72
|
throw new Error("signing request source repository mismatch");
|
|
64
73
|
}
|
|
65
74
|
if (expectedRuntimeSha && request.runtime.sha !== expectedRuntimeSha) {
|
|
66
75
|
throw new Error("signing request runtime SHA mismatch");
|
|
67
76
|
}
|
|
68
77
|
const key = `${request.source.sha}:${request.artifact.id}:${request.artifact.platform}`;
|
|
69
|
-
if (seen.has(key))
|
|
78
|
+
if (seen.has(key))
|
|
79
|
+
throw new Error(`duplicate artifact signing request: ${key}`);
|
|
70
80
|
seen.add(key);
|
|
71
81
|
const item = {
|
|
72
82
|
id: request.artifact.id,
|
|
73
83
|
slug: safeId(request.artifact.id),
|
|
74
84
|
request: path.relative(root, requestPath).split(path.sep).join("/"),
|
|
75
|
-
directory: path
|
|
76
|
-
|
|
85
|
+
directory: path
|
|
86
|
+
.relative(root, path.dirname(requestPath))
|
|
87
|
+
.split(path.sep)
|
|
88
|
+
.join("/"),
|
|
89
|
+
indexRoot:
|
|
90
|
+
path
|
|
91
|
+
.relative(root, path.dirname(indexPath))
|
|
92
|
+
.split(path.sep)
|
|
93
|
+
.join("/") || ".",
|
|
94
|
+
kind: request.artifact.kind,
|
|
95
|
+
transportFormat: request.artifact.transport?.format || "",
|
|
77
96
|
};
|
|
78
|
-
if (request.signature.profile === "detached-signature-v1")
|
|
79
|
-
|
|
80
|
-
else if (request.signature.profile === "
|
|
81
|
-
|
|
97
|
+
if (request.signature.profile === "detached-signature-v1")
|
|
98
|
+
matrices.detached.push(item);
|
|
99
|
+
else if (request.signature.profile === "apple-developer-id")
|
|
100
|
+
matrices.macos.push(item);
|
|
101
|
+
else if (request.signature.profile === "windows-authenticode")
|
|
102
|
+
matrices.windows.push(item);
|
|
103
|
+
else
|
|
104
|
+
throw new Error(
|
|
105
|
+
`unsupported signing authority profile: ${request.signature.profile}`,
|
|
106
|
+
);
|
|
82
107
|
}
|
|
83
108
|
}
|
|
84
|
-
for (const entries of Object.values(matrices))
|
|
109
|
+
for (const entries of Object.values(matrices))
|
|
110
|
+
entries.sort((a, b) => a.request.localeCompare(b.request));
|
|
85
111
|
writeGitHubOutputs({
|
|
86
112
|
"detached-matrix": JSON.stringify(matrices.detached),
|
|
87
113
|
"macos-matrix": JSON.stringify(matrices.macos),
|
|
@@ -91,11 +117,16 @@ export function inspectArtifactSigningRequests({
|
|
|
91
117
|
return matrices;
|
|
92
118
|
}
|
|
93
119
|
|
|
94
|
-
if (
|
|
120
|
+
if (
|
|
121
|
+
process.argv[1] &&
|
|
122
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
123
|
+
) {
|
|
95
124
|
try {
|
|
96
125
|
inspectArtifactSigningRequests();
|
|
97
126
|
} catch (error) {
|
|
98
|
-
console.error(
|
|
127
|
+
console.error(
|
|
128
|
+
`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
|
|
129
|
+
);
|
|
99
130
|
process.exitCode = 1;
|
|
100
131
|
}
|
|
101
132
|
}
|