@kungfu-tech/buildchain 3.0.5-alpha.8 → 3.0.6-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.
Files changed (51) hide show
  1. package/contracts/auditable-demo-scenario-v1.schema.json +11 -0
  2. package/dist/site/buildchain-contract.json +42 -22
  3. package/dist/site/buildchain-site.json +73 -27
  4. package/dist/site/capability-registry.json +2 -2
  5. package/dist/site/cli-registry.json +5 -3
  6. package/dist/site/controller-registry.json +19 -3
  7. package/dist/site/kfd-claims.json +58 -14
  8. package/dist/site/kfd-upstream-aggregate.json +1 -1
  9. package/dist/site/manual-registry.json +7 -6
  10. package/dist/site/node-api-registry.json +335 -8
  11. package/dist/site/page-registry.json +63 -17
  12. package/dist/site/public-surface-audit.json +94 -11
  13. package/dist/site/publication-authority-registry.json +21 -1
  14. package/dist/site/publication-registry.json +4 -4
  15. package/dist/site/site-manifest.json +10 -10
  16. package/dist/site/workflow-registry.json +53 -4
  17. package/docs/MAP.md +4 -1
  18. package/docs/auditable-demo.md +19 -3
  19. package/docs/cli-reference.md +15 -4
  20. package/docs/dev-alpha-candidate-patrol.md +14 -5
  21. package/docs/dev-qualification-patrol.md +108 -0
  22. package/docs/node-api-reference.md +42 -9
  23. package/docs/release-propagation.md +93 -0
  24. package/docs/reusable-build-surface.md +44 -12
  25. package/docs/stable-candidate-patrol.md +5 -2
  26. package/package.json +1 -1
  27. package/packages/core/buildchain-publication-authority.js +1 -0
  28. package/packages/core/release-propagation-agent-entry.js +185 -0
  29. package/packages/core/release-propagation-push.js +293 -0
  30. package/packages/core/release-propagation-stage-evidence.js +36 -0
  31. package/packages/core/release-propagation-work.js +1 -1
  32. package/packages/core/release-propagation.js +15 -0
  33. package/scripts/artifact-signing-controller-core.mjs +525 -0
  34. package/scripts/artifact-signing-controller.mjs +302 -0
  35. package/scripts/artifact-signing-delegation.mjs +51 -3
  36. package/scripts/auditable-demo-platform.mjs +59 -1
  37. package/scripts/auditable-demo-transport-smoke.mjs +176 -0
  38. package/scripts/build-standalone-binary.mjs +3 -1
  39. package/scripts/buildchain-cli-help.mjs +1 -1
  40. package/scripts/check-inventory.mjs +1 -0
  41. package/scripts/compiler-cache-evidence.mjs +90 -7
  42. package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
  43. package/scripts/dev-pr-auto-merge.mjs +4 -4
  44. package/scripts/dev-qualification-patrol.mjs +594 -0
  45. package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
  46. package/scripts/generate-site-bundle.mjs +3 -0
  47. package/scripts/inspect-artifact-signing-requests.mjs +12 -0
  48. package/scripts/release-propagation.mjs +73 -0
  49. package/scripts/run-lifecycle-core.mjs +25 -0
  50. package/scripts/site-capability-metadata.mjs +1 -1
  51. package/scripts/stable-candidate-patrol.mjs +30 -4
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import {
7
+ RECEIPT_CONTRACT,
8
+ artifactSigningControlRequestOutputs,
9
+ assertArtifactSigningControlRequestContext,
10
+ digestDocument,
11
+ normalizeControllerStatus,
12
+ readArtifactSigningControlRequest,
13
+ required,
14
+ sealArtifactSigningControlRequest,
15
+ validateArtifactSigningControlRequest,
16
+ validateArtifactSigningControllerReceipt,
17
+ } from "./artifact-signing-controller-core.mjs";
18
+ import {
19
+ assertArtifactSigningDelegationContext,
20
+ artifactSigningDelegationOutputs,
21
+ createArtifactSigningDelegation,
22
+ readArtifactSigningDelegation,
23
+ } from "./artifact-signing-delegation.mjs";
24
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
25
+
26
+ export {
27
+ artifactSigningCorrelation,
28
+ artifactSigningRequestRoot,
29
+ artifactSigningControlRequestOutputs,
30
+ assertArtifactSigningControlRequestContext,
31
+ createArtifactSigningControlRequest,
32
+ readArtifactSigningControlRequest,
33
+ sealArtifactSigningControlRequest,
34
+ validateArtifactSigningControlRequest,
35
+ validateArtifactSigningControllerReceipt,
36
+ } from "./artifact-signing-controller-core.mjs";
37
+
38
+ function authorityRuntimeShaFromEnvironment() {
39
+ return process.env.BUILDCHAIN_AUTHORITY_RUNTIME_SHA || "";
40
+ }
41
+
42
+ export function settleArtifactSigningControl({
43
+ request,
44
+ authorityStatus = process.env.BUILDCHAIN_AUTHORITY_STATUS || "failed",
45
+ authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
46
+ authorityRuntimeSha = authorityRuntimeShaFromEnvironment(),
47
+ authorityRunUrl = process.env.BUILDCHAIN_AUTHORITY_RUN_URL || "",
48
+ authorityResultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT ||
49
+ "",
50
+ authorityCorrelationId = process.env.BUILDCHAIN_AUTHORITY_CORRELATION_ID ||
51
+ "",
52
+ authorityConclusion = process.env.BUILDCHAIN_AUTHORITY_CONCLUSION || "",
53
+ controllerRepository = process.env.GITHUB_REPOSITORY,
54
+ controllerRunId = process.env.GITHUB_RUN_ID,
55
+ controllerRunAttempt = process.env.GITHUB_RUN_ATTEMPT || "1",
56
+ controllerJob = process.env.GITHUB_JOB || "artifact-signing-control",
57
+ controllerRunnerOs = process.env.RUNNER_OS || "Linux",
58
+ controllerStartedAt = process.env.BUILDCHAIN_CONTROLLER_STARTED_AT ||
59
+ new Date().toISOString(),
60
+ controllerCompletedAt = process.env.BUILDCHAIN_CONTROLLER_COMPLETED_AT ||
61
+ new Date().toISOString(),
62
+ receiptPath = process.env.BUILDCHAIN_SIGNING_CONTROLLER_RECEIPT_PATH,
63
+ delegationPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
64
+ } = {}) {
65
+ const control = validateArtifactSigningControlRequest(request);
66
+ const status = normalizeControllerStatus(authorityStatus);
67
+ const expectedStatus = control.request.count === 0 ? "skipped" : "succeeded";
68
+ const qualifying =
69
+ status === expectedStatus &&
70
+ (control.request.count === 0 || authorityConclusion === "success");
71
+ if (
72
+ authorityCorrelationId &&
73
+ authorityCorrelationId !== control.authority.correlationId
74
+ ) {
75
+ throw new Error("authority correlation does not match control request");
76
+ }
77
+ if (
78
+ authorityResultArtifact &&
79
+ authorityResultArtifact !== control.authority.resultArtifact
80
+ ) {
81
+ throw new Error("authority result artifact does not match control request");
82
+ }
83
+ const receipt = {
84
+ schemaVersion: 1,
85
+ contract: RECEIPT_CONTRACT,
86
+ requestDigest: control.digest,
87
+ source: control.source,
88
+ runtime: {
89
+ repository: control.runtime.repository,
90
+ sha: control.runtime.sha,
91
+ },
92
+ platform: control.platform,
93
+ request: control.request,
94
+ authority: {
95
+ repository: control.authority.repository,
96
+ runtimeSha: authorityRuntimeSha,
97
+ runId: control.request.count > 0 ? authorityRunId : "",
98
+ runUrl: control.request.count > 0 ? authorityRunUrl : "",
99
+ resultArtifact: control.request.count > 0 ? authorityResultArtifact : "",
100
+ correlationId: control.authority.correlationId,
101
+ conclusion:
102
+ control.request.count > 0
103
+ ? authorityConclusion || status
104
+ : "not-required",
105
+ },
106
+ controller: {
107
+ repository: controllerRepository,
108
+ runId: controllerRunId,
109
+ runAttempt: Number(controllerRunAttempt),
110
+ job: controllerJob,
111
+ runnerOs: controllerRunnerOs,
112
+ startedAt: controllerStartedAt,
113
+ completedAt: controllerCompletedAt,
114
+ status,
115
+ },
116
+ qualifying,
117
+ };
118
+ receipt.digest = digestDocument(receipt);
119
+ const validatedReceipt = validateArtifactSigningControllerReceipt(receipt);
120
+ const resolvedReceiptPath = path.resolve(
121
+ required(receiptPath, "controller receipt path"),
122
+ );
123
+ fs.mkdirSync(path.dirname(resolvedReceiptPath), { recursive: true });
124
+ fs.writeFileSync(
125
+ resolvedReceiptPath,
126
+ `${JSON.stringify(validatedReceipt, null, 2)}\n`,
127
+ );
128
+
129
+ let delegation = null;
130
+ if (validatedReceipt.qualifying) {
131
+ delegation = createArtifactSigningDelegation({
132
+ sourceRepository: control.source.repository,
133
+ sourceRunId: control.source.runId,
134
+ sourceRunAttempt: control.source.runAttempt,
135
+ sourceSha: control.source.sha,
136
+ sourceTreeSha: control.source.treeSha,
137
+ runtimeRepository: control.runtime.repository,
138
+ runtimeSha: control.runtime.sha,
139
+ platformId: control.platform.id,
140
+ platformName: control.platform.name,
141
+ requestCount: control.request.count,
142
+ requestArtifact: control.request.artifact,
143
+ requestRoot: control.request.root,
144
+ authorityRunId: validatedReceipt.authority.runId,
145
+ authorityRuntimeSha: validatedReceipt.authority.runtimeSha,
146
+ resultArtifact: validatedReceipt.authority.resultArtifact,
147
+ artifactName: control.artifact.name,
148
+ manifestArtifact: control.artifact.manifestArtifact,
149
+ diagnosticsArtifact: control.artifact.diagnosticsArtifact,
150
+ workingDirectory: control.workingDirectory,
151
+ controllerMode: "detached",
152
+ controllerReceiptDigest: validatedReceipt.digest,
153
+ });
154
+ const resolvedDelegationPath = path.resolve(
155
+ required(delegationPath, "delegation path"),
156
+ );
157
+ fs.mkdirSync(path.dirname(resolvedDelegationPath), { recursive: true });
158
+ fs.writeFileSync(
159
+ resolvedDelegationPath,
160
+ `${JSON.stringify(delegation, null, 2)}\n`,
161
+ );
162
+ }
163
+ return { receipt: validatedReceipt, delegation };
164
+ }
165
+
166
+ export function readArtifactSigningControllerReceipt(
167
+ inputPath = process.env.BUILDCHAIN_SIGNING_CONTROLLER_RECEIPT_PATH,
168
+ ) {
169
+ const target = path.resolve(required(inputPath, "controller receipt path"));
170
+ return validateArtifactSigningControllerReceipt(
171
+ JSON.parse(fs.readFileSync(target, "utf8")),
172
+ );
173
+ }
174
+
175
+ export function assertArtifactSigningControllerReceipt({
176
+ request,
177
+ receipt,
178
+ delegation,
179
+ }) {
180
+ const control = validateArtifactSigningControlRequest(request);
181
+ const result = validateArtifactSigningControllerReceipt(receipt);
182
+ const finalDelegation = assertArtifactSigningDelegationContext(delegation, {
183
+ sourceRepository: control.source.repository,
184
+ sourceRunId: control.source.runId,
185
+ sourceRunAttempt: control.source.runAttempt,
186
+ sourceSha: control.source.sha,
187
+ runtimeRepository: control.runtime.repository,
188
+ runtimeSha: control.runtime.sha,
189
+ platformId: control.platform.id,
190
+ });
191
+ const comparisons = [
192
+ [result.requestDigest, control.digest, "control request digest"],
193
+ [result.source.treeSha, control.source.treeSha, "source tree SHA"],
194
+ [result.request.root, control.request.root, "request root"],
195
+ [result.request.artifact, control.request.artifact, "request artifact"],
196
+ [
197
+ result.authority.correlationId,
198
+ control.authority.correlationId,
199
+ "correlation",
200
+ ],
201
+ [
202
+ result.authority.runId,
203
+ finalDelegation.authority.runId,
204
+ "authority run ID",
205
+ ],
206
+ [
207
+ result.authority.runtimeSha,
208
+ finalDelegation.authority.runtimeSha,
209
+ "authority runtime SHA",
210
+ ],
211
+ [
212
+ result.authority.resultArtifact,
213
+ finalDelegation.authority.resultArtifact,
214
+ "authority result artifact",
215
+ ],
216
+ [
217
+ finalDelegation.controller.receiptDigest,
218
+ result.digest,
219
+ "controller receipt digest",
220
+ ],
221
+ [
222
+ finalDelegation.request.root,
223
+ result.request.root,
224
+ "delegation request root",
225
+ ],
226
+ ];
227
+ for (const [actual, expected, label] of comparisons) {
228
+ if (actual !== expected) {
229
+ throw new Error(`artifact signing controller ${label} mismatch`);
230
+ }
231
+ }
232
+ if (!result.qualifying) {
233
+ throw new Error("artifact signing controller receipt is not qualifying");
234
+ }
235
+ return { request: control, receipt: result, delegation: finalDelegation };
236
+ }
237
+
238
+ function expectedContext() {
239
+ return {
240
+ sourceRepository: process.env.BUILDCHAIN_EXPECTED_SOURCE_REPOSITORY || "",
241
+ sourceRunId: process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ID || "",
242
+ sourceRunAttempt: process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ATTEMPT || "",
243
+ sourceSha: process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || "",
244
+ sourceTreeSha: process.env.BUILDCHAIN_EXPECTED_SOURCE_TREE_SHA || "",
245
+ runtimeRepository: process.env.BUILDCHAIN_EXPECTED_RUNTIME_REPOSITORY || "",
246
+ runtimeSha: process.env.BUILDCHAIN_EXPECTED_RUNTIME_SHA || "",
247
+ platformId: process.env.BUILDCHAIN_EXPECTED_PLATFORM_ID || "",
248
+ };
249
+ }
250
+
251
+ if (
252
+ process.argv[1] &&
253
+ import.meta.url === pathToFileURL(process.argv[1]).href
254
+ ) {
255
+ try {
256
+ const mode = process.argv[2] || "seal";
257
+ if (mode === "seal") {
258
+ sealArtifactSigningControlRequest();
259
+ } else if (mode === "outputs") {
260
+ const request = assertArtifactSigningControlRequestContext(
261
+ readArtifactSigningControlRequest(),
262
+ expectedContext(),
263
+ );
264
+ writeGitHubOutputs(artifactSigningControlRequestOutputs(request));
265
+ } else if (mode === "settle") {
266
+ const request = assertArtifactSigningControlRequestContext(
267
+ readArtifactSigningControlRequest(),
268
+ expectedContext(),
269
+ );
270
+ const { receipt, delegation } = settleArtifactSigningControl({ request });
271
+ writeGitHubOutputs({
272
+ "controller-status": receipt.controller.status,
273
+ "controller-receipt-digest": receipt.digest,
274
+ "controller-qualifying": String(receipt.qualifying),
275
+ "delegation-created": String(Boolean(delegation)),
276
+ });
277
+ } else if (mode === "verify") {
278
+ const request = assertArtifactSigningControlRequestContext(
279
+ readArtifactSigningControlRequest(),
280
+ expectedContext(),
281
+ );
282
+ const checked = assertArtifactSigningControllerReceipt({
283
+ request,
284
+ receipt: readArtifactSigningControllerReceipt(),
285
+ delegation: readArtifactSigningDelegation(),
286
+ });
287
+ writeGitHubOutputs({
288
+ ...artifactSigningDelegationOutputs(checked.delegation),
289
+ "request-root": checked.receipt.request.root,
290
+ "controller-receipt-digest": checked.receipt.digest,
291
+ "controller-status": checked.receipt.controller.status,
292
+ });
293
+ } else {
294
+ throw new Error(`unsupported artifact signing controller mode: ${mode}`);
295
+ }
296
+ } catch (error) {
297
+ console.error(
298
+ `::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
299
+ );
300
+ process.exitCode = 1;
301
+ }
302
+ }
@@ -22,6 +22,14 @@ function optional(value, label) {
22
22
  return normalized;
23
23
  }
24
24
 
25
+ function sha256Root(value, label) {
26
+ const normalized = optional(value, label);
27
+ if (normalized && !/^sha256:[0-9a-f]{64}$/u.test(normalized)) {
28
+ throw new Error(`${label} must be a canonical sha256 root`);
29
+ }
30
+ return normalized;
31
+ }
32
+
25
33
  function exactSha(value, label) {
26
34
  const normalized = required(value, label);
27
35
  if (!/^[0-9a-f]{40}$/u.test(normalized))
@@ -97,8 +105,13 @@ export function validateArtifactSigningDelegation(value) {
97
105
  requestCount > 0
98
106
  ? required(value.request?.artifact, "request.artifact")
99
107
  : optional(value.request?.artifact, "request.artifact"),
108
+ root: sha256Root(value.request?.root, "request.root"),
100
109
  },
101
110
  authority: {
111
+ runtimeSha:
112
+ requestCount > 0
113
+ ? exactSha(value.authority?.runtimeSha, "authority.runtimeSha")
114
+ : optional(value.authority?.runtimeSha, "authority.runtimeSha"),
102
115
  runId:
103
116
  requestCount > 0
104
117
  ? required(value.authority?.runId, "authority.runId")
@@ -129,15 +142,32 @@ export function validateArtifactSigningDelegation(value) {
129
142
  value.workingDirectory || ".",
130
143
  "workingDirectory",
131
144
  ),
145
+ controller: {
146
+ mode: optional(value.controller?.mode, "controller.mode"),
147
+ receiptDigest: sha256Root(
148
+ value.controller?.receiptDigest,
149
+ "controller.receiptDigest",
150
+ ),
151
+ },
132
152
  };
133
153
  if (
134
154
  requestCount === 0 &&
135
- (delegation.authority.runId || delegation.authority.resultArtifact)
155
+ (delegation.authority.runtimeSha ||
156
+ delegation.authority.runId ||
157
+ delegation.authority.resultArtifact)
136
158
  ) {
137
159
  throw new Error(
138
160
  "unsigned delegation must not contain authority result coordinates",
139
161
  );
140
162
  }
163
+ if (
164
+ Boolean(delegation.controller.mode) !==
165
+ Boolean(delegation.controller.receiptDigest)
166
+ ) {
167
+ throw new Error(
168
+ "delegation controller mode and receipt digest must be provided together",
169
+ );
170
+ }
141
171
  return delegation;
142
172
  }
143
173
 
@@ -153,12 +183,17 @@ export function createArtifactSigningDelegation({
153
183
  platformName = process.env.BUILDCHAIN_PLATFORM_NAME,
154
184
  requestCount = process.env.BUILDCHAIN_SIGNING_REQUEST_COUNT || "0",
155
185
  requestArtifact = process.env.BUILDCHAIN_SIGNING_REQUEST_ARTIFACT || "",
186
+ requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT_DIGEST || "",
156
187
  authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
188
+ authorityRuntimeSha = process.env.BUILDCHAIN_AUTHORITY_RUNTIME_SHA || "",
157
189
  resultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT || "",
158
190
  artifactName = process.env.BUILDCHAIN_ARTIFACT_NAME,
159
191
  manifestArtifact = process.env.BUILDCHAIN_MANIFEST_ARTIFACT_NAME,
160
192
  diagnosticsArtifact = process.env.BUILDCHAIN_DIAGNOSTICS_ARTIFACT_NAME,
161
193
  workingDirectory = process.env.BUILDCHAIN_SIGNING_CWD || ".",
194
+ controllerMode = process.env.BUILDCHAIN_SIGNING_CONTROLLER_MODE || "",
195
+ controllerReceiptDigest = process.env
196
+ .BUILDCHAIN_SIGNING_CONTROLLER_RECEIPT_DIGEST || "",
162
197
  } = {}) {
163
198
  return validateArtifactSigningDelegation({
164
199
  schemaVersion: 1,
@@ -172,10 +207,22 @@ export function createArtifactSigningDelegation({
172
207
  },
173
208
  runtime: { repository: runtimeRepository, sha: runtimeSha },
174
209
  platform: { id: platformId, name: platformName },
175
- request: { count: Number(requestCount), artifact: requestArtifact },
176
- authority: { runId: authorityRunId, resultArtifact },
210
+ request: {
211
+ count: Number(requestCount),
212
+ artifact: requestArtifact,
213
+ root: requestRoot,
214
+ },
215
+ authority: {
216
+ runtimeSha: authorityRuntimeSha,
217
+ runId: authorityRunId,
218
+ resultArtifact,
219
+ },
177
220
  artifact: { name: artifactName, manifestArtifact, diagnosticsArtifact },
178
221
  workingDirectory,
222
+ controller: {
223
+ mode: controllerMode,
224
+ receiptDigest: controllerReceiptDigest,
225
+ },
179
226
  });
180
227
  }
181
228
 
@@ -205,6 +252,7 @@ export function artifactSigningDelegationOutputs(delegation) {
205
252
  "request-count": String(value.request.count),
206
253
  "request-artifact": value.request.artifact,
207
254
  "authority-run-id": value.authority.runId,
255
+ "authority-runtime-sha": value.authority.runtimeSha,
208
256
  "result-artifact": value.authority.resultArtifact,
209
257
  "artifact-name": value.artifact.name,
210
258
  "manifest-artifact-name": value.artifact.manifestArtifact,
@@ -25,6 +25,7 @@ const RENDITIONS = [
25
25
  ];
26
26
  const STANDARD_MAX_SECONDS = 60;
27
27
  const LONG_FORM_MAX_SECONDS = 180;
28
+ const MAX_EXECUTABLE_FILES = 32;
28
29
 
29
30
  function durationPolicy(value = "standard") {
30
31
  requireValue(value === "standard" || value === "long-form", "scenario duration class is invalid");
@@ -143,6 +144,54 @@ function validateArtifact(artifact) {
143
144
  requireValue(Array.isArray(artifact.runtimeDependencies) && artifact.runtimeDependencies.length === 0, "scenario artifact must be standalone");
144
145
  }
145
146
 
147
+ function validateTransportSmoke(smoke) {
148
+ exactKeys(smoke, ["argv", "timeoutSeconds", "expectedExitCodes", "stdoutIncludes"], [], "scenario.transportSmoke");
149
+ requireValue(Array.isArray(smoke.argv) && smoke.argv.length >= 1 && smoke.argv.length <= 64 && smoke.argv.every((item) => typeof item === "string" && !item.includes("\0") && item.length <= 512), "scenario transport smoke argv is invalid");
150
+ requireValue(Number.isInteger(smoke.timeoutSeconds) && smoke.timeoutSeconds >= 1 && smoke.timeoutSeconds <= STANDARD_MAX_SECONDS, "scenario transport smoke timeout is invalid");
151
+ requireValue(Array.isArray(smoke.expectedExitCodes) && smoke.expectedExitCodes.length >= 1 && smoke.expectedExitCodes.length <= 4 && smoke.expectedExitCodes.every((item) => Number.isInteger(item) && item >= 0 && item <= 255), "scenario transport smoke expected exits are invalid");
152
+ requireValue(Array.isArray(smoke.stdoutIncludes) && smoke.stdoutIncludes.length <= 32 && smoke.stdoutIncludes.every((item) => typeof item === "string" && item.length >= 1 && item.length <= 256), "scenario transport smoke stdout assertions are invalid");
153
+ }
154
+
155
+ function validateExecutableClosure({ artifactRoot, scenario, requireExecutable }) {
156
+ const root = path.resolve(artifactRoot);
157
+ const artifact = scenario.artifact;
158
+ const metadata = readJson(inside(root, artifact.metadataPath, "artifact metadata path"), "binary metadata");
159
+ requireValue(metadata.contract === artifact.metadataContract, "binary metadata contract mismatch");
160
+ requireValue((metadata.platformId || metadata.platform) === artifact.platformId, "binary metadata platform mismatch");
161
+ requireValue(JSON.stringify(metadata.runtimeDependencies) === JSON.stringify(artifact.runtimeDependencies), "binary metadata runtime dependency mismatch");
162
+ requireValue(Array.isArray(metadata.executableFiles) && metadata.executableFiles.length >= 1 && metadata.executableFiles.length <= MAX_EXECUTABLE_FILES, "binary metadata executableFiles must be a bounded non-empty array");
163
+ const declared = new Set();
164
+ const files = metadata.executableFiles.map((entry, index) => {
165
+ const label = `binary metadata executableFiles[${index}]`;
166
+ exactKeys(entry, ["path", "sha256"], [], label);
167
+ requireValue(!declared.has(entry.path), `${label}.path is repeated`);
168
+ declared.add(entry.path);
169
+ requireValue(/^[0-9a-f]{64}$/u.test(entry.sha256), `${label}.sha256 is invalid`);
170
+ const file = inside(root, entry.path, `${label}.path`);
171
+ const bytes = regular(file, label, 256 * 1024 * 1024);
172
+ requireValue(rootBytes(bytes) === `sha256:${entry.sha256}`, `${label} digest differs from exact artifact metadata`);
173
+ if (requireExecutable) requireValue((fs.statSync(file).mode & 0o111) !== 0, `${label} is not executable`);
174
+ return { path: entry.path, file, sha256: entry.sha256 };
175
+ });
176
+ requireValue(declared.has(artifact.binaryPath), "binary metadata executableFiles must include scenario.artifact.binaryPath");
177
+ const binary = files.find((entry) => entry.path === artifact.binaryPath);
178
+ const metadataBinarySha256 = String(metadata.sha256 || "").replace(/^sha256:/u, "");
179
+ requireValue(/^[0-9a-f]{64}$/u.test(metadataBinarySha256) && metadataBinarySha256 === binary.sha256, "binary digest differs from exact artifact metadata");
180
+ return { metadata, files };
181
+ }
182
+
183
+ export function prepareArtifact({ artifactRoot, scenarioPath }) {
184
+ const scenario = validateScenario(readJson(path.resolve(scenarioPath), "scenario"));
185
+ const closure = validateExecutableClosure({ artifactRoot, scenario, requireExecutable: false });
186
+ for (const entry of closure.files) fs.chmodSync(entry.file, 0o755);
187
+ validateExecutableClosure({ artifactRoot, scenario, requireExecutable: true });
188
+ return {
189
+ executableFiles: closure.files.map(({ path: relative, sha256 }) => ({ path: relative, sha256 })),
190
+ metadataRoot: rootJson(closure.metadata),
191
+ authority: { classification: "artifact-mode-restoration", grants: [], nonAuthorities: NON_AUTHORITIES },
192
+ };
193
+ }
194
+
146
195
  function validateExecution(execution) {
147
196
  exactKeys(execution, ["deterministic", "network", "secrets", "totalTimeoutSeconds", "environment"], ["durationClass"], "scenario.execution");
148
197
  requireValue(execution.deterministic === true && execution.network === "none" && execution.secrets === "none", "scenario execution must be deterministic, network-disabled, and secret-free");
@@ -185,7 +234,7 @@ function validateDemo(demo, index, demoIds, maximumSeconds) {
185
234
  }
186
235
 
187
236
  export function validateScenario(value) {
188
- exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], [], "scenario");
237
+ exactKeys(value, ["schema", "product", "artifact", "execution", "renditions", "demos", "publication", "authority"], ["transportSmoke"], "scenario");
189
238
  requireValue(value.schema === "buildchain.declarative-binary-demo/v1", "unsupported scenario schema");
190
239
  validateProduct(value.product);
191
240
  validateArtifact(value.artifact);
@@ -194,6 +243,7 @@ export function validateScenario(value) {
194
243
  requireValue(Array.isArray(value.demos) && value.demos.length >= 1 && value.demos.length <= 8, "scenario requires 1 through 8 demos");
195
244
  const demoIds = new Set();
196
245
  value.demos.forEach((demo, index) => validateDemo(demo, index, demoIds, executionPolicy.maximumSeconds));
246
+ if (value.transportSmoke) validateTransportSmoke(value.transportSmoke);
197
247
  exactKeys(value.publication, ["evidencePath", "readmePath", "marker"], [], "scenario.publication");
198
248
  inside("/repository", value.publication.evidencePath, "scenario.publication.evidencePath");
199
249
  inside("/repository", value.publication.readmePath, "scenario.publication.readmePath");
@@ -474,6 +524,14 @@ function main(argv = process.argv.slice(2)) {
474
524
  process.stdout.write(stableJson(scenario.publication));
475
525
  return;
476
526
  }
527
+ if (command === "prepare-artifact") {
528
+ const result = prepareArtifact({
529
+ artifactRoot: path.resolve(args["artifact-root"]),
530
+ scenarioPath: path.resolve(args.scenario),
531
+ });
532
+ process.stdout.write(stableJson({ ok: true, ...result }));
533
+ return;
534
+ }
477
535
  if (command === "adapt") {
478
536
  const result = adaptCapture({ artifactRoot: path.resolve(args["artifact-root"]), output: path.resolve(args.output) });
479
537
  process.stdout.write(stableJson({ ok: true, ...result }));
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { spawnSync } from "node:child_process";
5
+ import crypto from "node:crypto";
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+
11
+ import {
12
+ prepareArtifact,
13
+ validateScenario,
14
+ } from "./auditable-demo-platform.mjs";
15
+
16
+ const NON_AUTHORITIES = [
17
+ "first-party-identity",
18
+ "system-identity",
19
+ "kfd-compliance",
20
+ "product-system-metadata",
21
+ "package-metadata",
22
+ "registry-history",
23
+ "scan-output",
24
+ "standalone-generation",
25
+ ];
26
+
27
+ function fail(message) {
28
+ throw new Error(`auditable demo transport smoke: ${message}`);
29
+ }
30
+
31
+ function inside(root, relative, label) {
32
+ if (typeof relative !== "string" || !relative || path.isAbsolute(relative))
33
+ fail(`${label} must be relative`);
34
+ const resolvedRoot = path.resolve(root);
35
+ const resolved = path.resolve(resolvedRoot, relative);
36
+ if (
37
+ resolved === resolvedRoot ||
38
+ !resolved.startsWith(`${resolvedRoot}${path.sep}`)
39
+ )
40
+ fail(`${label} escapes its root`);
41
+ return resolved;
42
+ }
43
+
44
+ function removeExecuteBits(root) {
45
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
46
+ const file = path.join(root, entry.name);
47
+ if (entry.isDirectory()) removeExecuteBits(file);
48
+ else if (entry.isFile())
49
+ fs.chmodSync(file, fs.statSync(file).mode & ~0o111);
50
+ }
51
+ }
52
+
53
+ function isolatedEnvironment(home, declared) {
54
+ const env = {
55
+ PATH: "/usr/local/bin:/usr/bin:/bin",
56
+ HOME: home,
57
+ XDG_CACHE_HOME: path.join(home, ".cache"),
58
+ XDG_CONFIG_HOME: path.join(home, ".config"),
59
+ XDG_DATA_HOME: path.join(home, ".local/share"),
60
+ XDG_STATE_HOME: path.join(home, ".local/state"),
61
+ LANG: "C.UTF-8",
62
+ LC_ALL: "C.UTF-8",
63
+ TZ: "UTC",
64
+ CI: "true",
65
+ TERM: "xterm-256color",
66
+ COLORTERM: "truecolor",
67
+ FORCE_COLOR: "3",
68
+ };
69
+ for (const [key, value] of Object.entries(declared)) {
70
+ if (Object.hasOwn(env, key))
71
+ fail(
72
+ `scenario environment may not override the isolated baseline: ${key}`,
73
+ );
74
+ env[key] = value;
75
+ }
76
+ return env;
77
+ }
78
+
79
+ export function runTransportSmoke({ artifactRoot, scenarioPath }) {
80
+ if (process.platform === "win32")
81
+ fail("transport smoke currently requires a POSIX host");
82
+ const root = path.resolve(artifactRoot);
83
+ const scenario = validateScenario(
84
+ JSON.parse(fs.readFileSync(path.resolve(scenarioPath), "utf8")),
85
+ );
86
+ if (!scenario.transportSmoke) fail("scenario.transportSmoke is required");
87
+ const distributionRelative = path.posix.dirname(
88
+ scenario.artifact.metadataPath,
89
+ );
90
+ const sourceDistribution =
91
+ distributionRelative === "."
92
+ ? root
93
+ : inside(root, distributionRelative, "distribution root");
94
+ const temporaryRoot = fs.mkdtempSync(
95
+ path.join(os.tmpdir(), "buildchain-demo-transport-"),
96
+ );
97
+ try {
98
+ const transportedRoot = path.join(temporaryRoot, "artifact");
99
+ const transportedDistribution =
100
+ distributionRelative === "."
101
+ ? transportedRoot
102
+ : path.join(transportedRoot, ...distributionRelative.split("/"));
103
+ fs.mkdirSync(path.dirname(transportedDistribution), { recursive: true });
104
+ fs.cpSync(sourceDistribution, transportedDistribution, {
105
+ recursive: true,
106
+ dereference: false,
107
+ preserveTimestamps: true,
108
+ verbatimSymlinks: true,
109
+ });
110
+ removeExecuteBits(transportedDistribution);
111
+ const prepared = prepareArtifact({
112
+ artifactRoot: transportedRoot,
113
+ scenarioPath,
114
+ });
115
+ const binary = inside(
116
+ transportedRoot,
117
+ scenario.artifact.binaryPath,
118
+ "transport smoke binary",
119
+ );
120
+ const workspace = path.join(temporaryRoot, "workspace");
121
+ const home = path.join(temporaryRoot, "home");
122
+ fs.mkdirSync(workspace);
123
+ fs.mkdirSync(home);
124
+ const smoke = scenario.transportSmoke;
125
+ const result = spawnSync(binary, smoke.argv, {
126
+ cwd: workspace,
127
+ env: isolatedEnvironment(home, scenario.execution.environment),
128
+ encoding: "utf8",
129
+ timeout: smoke.timeoutSeconds * 1000,
130
+ maxBuffer: 4 * 1024 * 1024,
131
+ });
132
+ const output = `${result.stdout || ""}${result.stderr || ""}`;
133
+ if (result.error) fail(`could not execute: ${result.error.message}`);
134
+ if (!smoke.expectedExitCodes.includes(result.status))
135
+ fail(`exited with ${result.status}; output: ${output.slice(-4096)}`);
136
+ for (const expected of smoke.stdoutIncludes)
137
+ if (!output.includes(expected)) fail(`output is missing: ${expected}`);
138
+ return {
139
+ schema: "buildchain.declarative-demo-transport-smoke/v1",
140
+ status: "passed",
141
+ executableFiles: prepared.executableFiles,
142
+ argv: smoke.argv,
143
+ exitCode: result.status,
144
+ outputRoot: `sha256:${crypto.createHash("sha256").update(output).digest("hex")}`,
145
+ authority: {
146
+ classification: "pre-upload-transport-diagnostic",
147
+ grants: [],
148
+ nonAuthorities: NON_AUTHORITIES,
149
+ },
150
+ };
151
+ } finally {
152
+ fs.rmSync(temporaryRoot, { recursive: true, force: true });
153
+ }
154
+ }
155
+
156
+ function parseArgs(argv) {
157
+ const values = {};
158
+ for (let index = 0; index < argv.length; index += 2)
159
+ values[argv[index].replace(/^--/u, "")] = argv[index + 1];
160
+ return values;
161
+ }
162
+
163
+ if (
164
+ process.argv[1] &&
165
+ import.meta.url === pathToFileURL(process.argv[1]).href
166
+ ) {
167
+ try {
168
+ const args = parseArgs(process.argv.slice(2));
169
+ process.stdout.write(
170
+ `${JSON.stringify({ ok: true, ...runTransportSmoke({ artifactRoot: args["artifact-root"], scenarioPath: args.scenario }) }, null, 2)}\n`,
171
+ );
172
+ } catch (error) {
173
+ console.error(error instanceof Error ? error.message : String(error));
174
+ process.exitCode = 1;
175
+ }
176
+ }