@kungfu-tech/buildchain 4.0.1-alpha.4 → 4.0.1-alpha.5
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/architecture/decisions/0002-next-development-transition.md +101 -0
- package/architecture/internal-capabilities.json +52 -3
- package/architecture/maintainability-policy.json +22 -10
- package/architecture/v3-core-mechanism-inventory.json +89 -0
- package/architecture/v4-capability-state-machine-manifest.json +73 -0
- package/architecture/v4-compatibility-facts-parity.json +77 -0
- package/architecture/v4-next-development-parity.json +192 -0
- package/architecture/v4-release-train-parity.json +1 -0
- package/architecture/v4-tail-reseal-parity.json +150 -0
- package/bin/buildchain.mjs +16 -1
- package/bin/internal/command-registry.mjs +2 -0
- package/bin/internal/compatibility-facts-cli.mjs +101 -0
- package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
- package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
- package/contracts/next-development-request-v1.schema.json +66 -0
- package/contracts/next-development-transition-v1.schema.json +292 -0
- package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
- package/contracts/v4-tail-reseal-v1.schema.json +276 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +1388 -23
- package/dist/site/buildchain-site.json +257 -19
- package/dist/site/capability-registry.json +10 -8
- package/dist/site/cli-registry.json +106 -0
- package/dist/site/kfd-claims.json +187 -12
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +35 -4
- package/dist/site/node-api-registry.json +6579 -4493
- package/dist/site/page-registry.json +233 -11
- package/dist/site/public-surface-audit.json +241 -7
- package/dist/site/publication-authority-registry.json +6 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
- package/dist/site/site-manifest.json +24 -8
- package/dist/site/workflow-registry.json +36 -4
- package/docs/MAP.md +4 -1
- package/docs/cli-reference.md +125 -0
- package/docs/dev-alpha-candidate-patrol.md +8 -0
- package/docs/next-development-transition.md +118 -0
- package/docs/node-api-reference.md +250 -117
- package/docs/v4-compatibility-facts.md +91 -0
- package/docs/v4-tail-reseal.md +73 -0
- package/docs/versioning.md +4 -3
- package/package.json +14 -5
- package/packages/core/buildchain-agent-manuals.js +2 -0
- package/packages/core/buildchain-compatibility-authority.js +336 -0
- package/packages/core/buildchain-compatibility-fact.js +394 -0
- package/packages/core/buildchain-compatibility-facts.json +217 -0
- package/packages/core/buildchain-compatibility-proof.js +566 -0
- package/packages/core/buildchain-contract.js +93 -141
- package/packages/core/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +55 -0
- package/packages/core/dev-alpha-candidate-selection.js +10 -2
- package/packages/core/dev-delivery-execution-transfer.js +21 -1
- package/packages/core/dev-delivery-process-boundary.js +19 -6
- package/packages/core/dev-delivery-provider-heartbeat.js +18 -6
- package/packages/core/kungfu-temporal-fact.js +557 -0
- package/packages/core/next-development-candidate-reservation.js +186 -0
- package/packages/core/next-development-controller.js +728 -0
- package/packages/core/next-development-projection.js +287 -0
- package/packages/core/next-development-transition.js +738 -0
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper.js +16 -5
- package/packages/core/v4-canonical-contracts.js +5 -0
- package/packages/core/v4-floating-consumer-policy.js +29 -9
- package/packages/core/v4-tail-reseal-contract.js +26 -0
- package/packages/core/v4-tail-reseal-github.js +178 -0
- package/packages/core/v4-tail-reseal-receipt.js +225 -0
- package/packages/core/v4-tail-reseal.js +594 -0
- package/scripts/buildchain-cli-help.mjs +20 -0
- package/scripts/check-inventory.mjs +11 -0
- package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
- package/scripts/generate-next-development-guidance.mjs +49 -0
- package/scripts/generate-site-bundle.mjs +16 -3
- package/scripts/init-repo.mjs +22 -4
- package/scripts/next-development-self-dogfood-harness.mjs +460 -0
- package/scripts/next-development-self-dogfood.mjs +597 -0
- package/scripts/next-development-transition.mjs +47 -0
- package/scripts/site-capability-metadata.mjs +12 -0
- package/scripts/v4-consumer-policy.mjs +1 -5
- package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
- package/scripts/v4-tail-reseal.mjs +416 -0
|
@@ -139,10 +139,6 @@ export function certifyCommand(options = {}) {
|
|
|
139
139
|
stableLockPath,
|
|
140
140
|
alphaLockPath,
|
|
141
141
|
});
|
|
142
|
-
const selectedLock =
|
|
143
|
-
receipt?.invocation?.visibleSelector === "v4-alpha"
|
|
144
|
-
? readJson(path.join(callerRoot, alphaLockPath))
|
|
145
|
-
: readJson(path.join(callerRoot, stableLockPath));
|
|
146
142
|
const authorityScan = scanV4FloatingConsumerPolicy({
|
|
147
143
|
root: callerRoot,
|
|
148
144
|
repository,
|
|
@@ -150,7 +146,7 @@ export function certifyCommand(options = {}) {
|
|
|
150
146
|
invokedWorkflow,
|
|
151
147
|
invocationSourcePath: receipt?.invocation?.sourcePath,
|
|
152
148
|
expectedInvocationChannel: receipt?.invocation?.channel,
|
|
153
|
-
resolvedWorkflowSha:
|
|
149
|
+
resolvedWorkflowSha: receipt?.invocation?.resolvedWorkflowSha,
|
|
154
150
|
resolvedRuntimeSha,
|
|
155
151
|
stableLockPath,
|
|
156
152
|
alphaLockPath,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
10
|
+
import { verifyV4TailResealPlatform } from "./v4-tail-reseal.mjs";
|
|
11
|
+
|
|
12
|
+
function flag(args, name, fallback = "") {
|
|
13
|
+
const index = args.indexOf(`--${name}`);
|
|
14
|
+
return index < 0 ? fallback : String(args[index + 1] || "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fileRoot(file) {
|
|
18
|
+
return `sha256:${crypto
|
|
19
|
+
.createHash("sha256")
|
|
20
|
+
.update(fs.readFileSync(file))
|
|
21
|
+
.digest("hex")}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function writeCandidate(root, request, content) {
|
|
25
|
+
const platform = request.platforms.find(({ id }) => id === "macos-arm64");
|
|
26
|
+
const payload = path.join(root, "product/release/macos-arm64/payload.bin");
|
|
27
|
+
fs.mkdirSync(path.dirname(payload), { recursive: true });
|
|
28
|
+
fs.writeFileSync(payload, content);
|
|
29
|
+
const relative = path.relative(root, payload);
|
|
30
|
+
const file = {
|
|
31
|
+
path: relative,
|
|
32
|
+
size: fs.statSync(payload).size,
|
|
33
|
+
digest: fileRoot(payload),
|
|
34
|
+
};
|
|
35
|
+
const manifest = {
|
|
36
|
+
contract: "kungfu-buildchain-artifact",
|
|
37
|
+
artifactName: platform.artifactName,
|
|
38
|
+
git: {
|
|
39
|
+
repository: request.repository,
|
|
40
|
+
sha: request.source.sha,
|
|
41
|
+
treeSha: request.source.treeSha,
|
|
42
|
+
runId: String(request.source.runId),
|
|
43
|
+
runAttempt: String(request.source.runAttempt),
|
|
44
|
+
},
|
|
45
|
+
platform: { id: platform.id, name: platform.name },
|
|
46
|
+
files: [
|
|
47
|
+
{
|
|
48
|
+
path: relative,
|
|
49
|
+
size: file.size,
|
|
50
|
+
sha256: file.digest.replace(/^sha256:/u, ""),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
summary: { fileCount: 1, totalBytes: file.size },
|
|
54
|
+
};
|
|
55
|
+
const manifestPath = path.join(root, "evidence/macos-arm64/manifest.json");
|
|
56
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
58
|
+
return {
|
|
59
|
+
artifactRoot: v4ContentRoot("tail-reseal-artifact-files", [file]),
|
|
60
|
+
manifestRoot: fileRoot(manifestPath),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function rehearseV4TailResealMacos({
|
|
65
|
+
fixturePath = "contracts/fixtures/v4-tail-reseal-v1/valid.json",
|
|
66
|
+
outputPath = "",
|
|
67
|
+
} = {}) {
|
|
68
|
+
const request = JSON.parse(
|
|
69
|
+
fs.readFileSync(path.resolve(fixturePath), "utf8"),
|
|
70
|
+
);
|
|
71
|
+
const workRoot = fs.mkdtempSync(
|
|
72
|
+
path.join(os.tmpdir(), "buildchain-v4-tail-macos-"),
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
const retainedRoots = writeCandidate(workRoot, request, "unsigned");
|
|
76
|
+
const platform = request.platforms.find(({ id }) => id === "macos-arm64");
|
|
77
|
+
Object.assign(platform, retainedRoots);
|
|
78
|
+
const retained = verifyV4TailResealPlatform({
|
|
79
|
+
request,
|
|
80
|
+
platformId: "macos-arm64",
|
|
81
|
+
artifactRoot: workRoot,
|
|
82
|
+
mode: "retained",
|
|
83
|
+
});
|
|
84
|
+
writeCandidate(workRoot, request, "signed-and-notarized");
|
|
85
|
+
const resealed = verifyV4TailResealPlatform({
|
|
86
|
+
request,
|
|
87
|
+
platformId: "macos-arm64",
|
|
88
|
+
artifactRoot: workRoot,
|
|
89
|
+
mode: "resealed",
|
|
90
|
+
providerReadbackRoot: request.signing.providerReadbackRoot,
|
|
91
|
+
});
|
|
92
|
+
const payload = {
|
|
93
|
+
schema: "kungfu-buildchain-v4-tail-reseal-macos-rehearsal/v1",
|
|
94
|
+
platformId: "macos-arm64",
|
|
95
|
+
retained,
|
|
96
|
+
resealed,
|
|
97
|
+
buildStagesExecuted: 0,
|
|
98
|
+
signingFinalizationExecutions: 1,
|
|
99
|
+
providerReadbackRoot: request.signing.providerReadbackRoot,
|
|
100
|
+
};
|
|
101
|
+
const evidence = {
|
|
102
|
+
...payload,
|
|
103
|
+
evidenceRoot: v4ContentRoot("tail-reseal-admission", payload),
|
|
104
|
+
};
|
|
105
|
+
if (outputPath) {
|
|
106
|
+
const output = path.resolve(outputPath);
|
|
107
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
108
|
+
fs.writeFileSync(output, `${JSON.stringify(evidence, null, 2)}\n`);
|
|
109
|
+
}
|
|
110
|
+
return evidence;
|
|
111
|
+
} finally {
|
|
112
|
+
fs.rmSync(workRoot, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
process.argv[1] &&
|
|
118
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
119
|
+
) {
|
|
120
|
+
try {
|
|
121
|
+
const evidence = rehearseV4TailResealMacos({
|
|
122
|
+
fixturePath: flag(
|
|
123
|
+
process.argv.slice(2),
|
|
124
|
+
"fixture",
|
|
125
|
+
"contracts/fixtures/v4-tail-reseal-v1/valid.json",
|
|
126
|
+
),
|
|
127
|
+
outputPath: flag(process.argv.slice(2), "output"),
|
|
128
|
+
});
|
|
129
|
+
process.stdout.write(`${JSON.stringify(evidence, null, 2)}\n`);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error(`v4-tail-reseal-macos-rehearsal: ${error.message}`);
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
V4_TAIL_RESEAL_PLATFORMS,
|
|
10
|
+
normalizeV4TailResealRequest,
|
|
11
|
+
planV4TailReseal,
|
|
12
|
+
} from "../packages/core/v4-tail-reseal.js";
|
|
13
|
+
import { createV4TailResealReceipt } from "../packages/core/v4-tail-reseal-receipt.js";
|
|
14
|
+
import { validateV4TailResealGitHubEvidence } from "../packages/core/v4-tail-reseal-github.js";
|
|
15
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
16
|
+
|
|
17
|
+
function flag(args, name, fallback = "") {
|
|
18
|
+
const index = args.indexOf(`--${name}`);
|
|
19
|
+
return index < 0 ? fallback : String(args[index + 1] || "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function required(value, label) {
|
|
23
|
+
const normalized = String(value || "").trim();
|
|
24
|
+
if (!normalized) throw new Error(`${label} is required`);
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJson(file, label = file) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new Error(`could not read ${label}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeJson(file, value) {
|
|
37
|
+
const output = path.resolve(file);
|
|
38
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
39
|
+
fs.writeFileSync(output, `${JSON.stringify(value, null, 2)}\n`);
|
|
40
|
+
return output;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sha256File(file) {
|
|
44
|
+
const hash = crypto.createHash("sha256");
|
|
45
|
+
const descriptor = fs.openSync(file, "r");
|
|
46
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
47
|
+
try {
|
|
48
|
+
for (;;) {
|
|
49
|
+
const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
50
|
+
if (count === 0) break;
|
|
51
|
+
hash.update(buffer.subarray(0, count));
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
fs.closeSync(descriptor);
|
|
55
|
+
}
|
|
56
|
+
return `sha256:${hash.digest("hex")}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function appendOutputs(values) {
|
|
60
|
+
if (!process.env.GITHUB_OUTPUT) return;
|
|
61
|
+
fs.appendFileSync(
|
|
62
|
+
process.env.GITHUB_OUTPUT,
|
|
63
|
+
`${Object.entries(values)
|
|
64
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
65
|
+
.join("\n")}\n`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function githubJson(repository, apiPath, token) {
|
|
70
|
+
const response = await fetch(
|
|
71
|
+
`https://api.github.com/repos/${repository}${apiPath}`,
|
|
72
|
+
{
|
|
73
|
+
headers: {
|
|
74
|
+
accept: "application/vnd.github+json",
|
|
75
|
+
authorization: `Bearer ${required(token, "GITHUB_TOKEN")}`,
|
|
76
|
+
"x-github-api-version": "2022-11-28",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
throw new Error(`GitHub API ${apiPath} failed: HTTP ${response.status}`);
|
|
82
|
+
return response.json();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function pagedGithubItems(repository, apiPath, key, token) {
|
|
86
|
+
const values = [];
|
|
87
|
+
for (let page = 1; ; page += 1) {
|
|
88
|
+
const separator = apiPath.includes("?") ? "&" : "?";
|
|
89
|
+
const response = await githubJson(
|
|
90
|
+
repository,
|
|
91
|
+
`${apiPath}${separator}per_page=100&page=${page}`,
|
|
92
|
+
token,
|
|
93
|
+
);
|
|
94
|
+
const items = Array.isArray(response?.[key]) ? response[key] : [];
|
|
95
|
+
values.push(...items);
|
|
96
|
+
if (items.length < 100) return values;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function admitFromGitHub(request) {
|
|
101
|
+
const token = required(process.env.GITHUB_TOKEN, "GITHUB_TOKEN");
|
|
102
|
+
const [
|
|
103
|
+
sourceCommit,
|
|
104
|
+
sourceRun,
|
|
105
|
+
sourceJobs,
|
|
106
|
+
sourceArtifacts,
|
|
107
|
+
signingRun,
|
|
108
|
+
signingArtifacts,
|
|
109
|
+
] = await Promise.all([
|
|
110
|
+
githubJson(request.repository, `/git/commits/${request.source.sha}`, token),
|
|
111
|
+
githubJson(
|
|
112
|
+
request.repository,
|
|
113
|
+
`/actions/runs/${request.source.runId}`,
|
|
114
|
+
token,
|
|
115
|
+
),
|
|
116
|
+
pagedGithubItems(
|
|
117
|
+
request.repository,
|
|
118
|
+
`/actions/runs/${request.source.runId}/jobs?filter=latest`,
|
|
119
|
+
"jobs",
|
|
120
|
+
token,
|
|
121
|
+
),
|
|
122
|
+
pagedGithubItems(
|
|
123
|
+
request.repository,
|
|
124
|
+
`/actions/runs/${request.source.runId}/artifacts`,
|
|
125
|
+
"artifacts",
|
|
126
|
+
token,
|
|
127
|
+
),
|
|
128
|
+
githubJson(
|
|
129
|
+
request.signing.authorityRepository,
|
|
130
|
+
`/actions/runs/${request.signing.authorityRunId}`,
|
|
131
|
+
token,
|
|
132
|
+
),
|
|
133
|
+
pagedGithubItems(
|
|
134
|
+
request.signing.authorityRepository,
|
|
135
|
+
`/actions/runs/${request.signing.authorityRunId}/artifacts`,
|
|
136
|
+
"artifacts",
|
|
137
|
+
token,
|
|
138
|
+
),
|
|
139
|
+
]);
|
|
140
|
+
return validateV4TailResealGitHubEvidence({
|
|
141
|
+
request,
|
|
142
|
+
sourceCommit,
|
|
143
|
+
sourceRun,
|
|
144
|
+
sourceJobs,
|
|
145
|
+
sourceArtifacts,
|
|
146
|
+
signingRun,
|
|
147
|
+
signingArtifacts,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function locateManifest(root, platformId) {
|
|
152
|
+
const matches = [];
|
|
153
|
+
const visit = (directory) => {
|
|
154
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
155
|
+
const absolute = path.join(directory, entry.name);
|
|
156
|
+
if (entry.isDirectory()) visit(absolute);
|
|
157
|
+
else if (entry.name === "manifest.json") {
|
|
158
|
+
const value = readJson(absolute);
|
|
159
|
+
if (
|
|
160
|
+
value?.contract === "kungfu-buildchain-artifact" &&
|
|
161
|
+
value?.platform?.id === platformId
|
|
162
|
+
)
|
|
163
|
+
matches.push({ absolute, value });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
visit(root);
|
|
168
|
+
if (matches.length !== 1)
|
|
169
|
+
throw new Error(
|
|
170
|
+
`expected exactly one ${platformId} manifest under ${root}, found ${matches.length}`,
|
|
171
|
+
);
|
|
172
|
+
return matches[0];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resolvePayload(root, relative, platformId) {
|
|
176
|
+
const absolute = path.resolve(root, relative);
|
|
177
|
+
if (
|
|
178
|
+
!absolute.startsWith(`${root}${path.sep}`) ||
|
|
179
|
+
!fs.existsSync(absolute) ||
|
|
180
|
+
!fs.statSync(absolute).isFile()
|
|
181
|
+
)
|
|
182
|
+
throw new Error(`${platformId} manifest file is missing: ${relative}`);
|
|
183
|
+
return absolute;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function verifyV4TailResealPlatform({
|
|
187
|
+
request,
|
|
188
|
+
platformId,
|
|
189
|
+
artifactRoot,
|
|
190
|
+
mode = "retained",
|
|
191
|
+
providerReadbackRoot = null,
|
|
192
|
+
} = {}) {
|
|
193
|
+
const normalized = normalizeV4TailResealRequest(request);
|
|
194
|
+
const platform = normalized.platforms.find(({ id }) => id === platformId);
|
|
195
|
+
if (!platform)
|
|
196
|
+
throw new Error(`tail reseal request does not bind platform ${platformId}`);
|
|
197
|
+
if (!new Set(["retained", "resealed"]).has(mode))
|
|
198
|
+
throw new Error(
|
|
199
|
+
"tail reseal verification mode must be retained or resealed",
|
|
200
|
+
);
|
|
201
|
+
if (mode === "resealed" && platformId !== "macos-arm64")
|
|
202
|
+
throw new Error("only macos-arm64 may produce a resealed byte set");
|
|
203
|
+
const root = path.resolve(artifactRoot || ".");
|
|
204
|
+
const { absolute: manifestPath, value: manifest } = locateManifest(
|
|
205
|
+
root,
|
|
206
|
+
platformId,
|
|
207
|
+
);
|
|
208
|
+
if (manifest.artifactName !== platform.artifactName)
|
|
209
|
+
throw new Error(`${platformId} manifest artifact name mismatch`);
|
|
210
|
+
if (
|
|
211
|
+
manifest.git?.repository !== normalized.repository ||
|
|
212
|
+
manifest.git?.sha !== normalized.source.sha ||
|
|
213
|
+
manifest.git?.treeSha !== normalized.source.treeSha
|
|
214
|
+
)
|
|
215
|
+
throw new Error(`${platformId} manifest source mismatch`);
|
|
216
|
+
if (
|
|
217
|
+
Number(manifest.git?.runId) !== normalized.source.runId ||
|
|
218
|
+
Number(manifest.git?.runAttempt) !== normalized.source.runAttempt
|
|
219
|
+
)
|
|
220
|
+
throw new Error(`${platformId} manifest original run mismatch`);
|
|
221
|
+
const files = (Array.isArray(manifest.files) ? manifest.files : [])
|
|
222
|
+
.map((file, index) => {
|
|
223
|
+
const relative = required(
|
|
224
|
+
file.path || file.name,
|
|
225
|
+
`${platformId} manifest files[${index}] path`,
|
|
226
|
+
);
|
|
227
|
+
const absolute = resolvePayload(root, relative, platformId);
|
|
228
|
+
const size = fs.statSync(absolute).size;
|
|
229
|
+
if (Number(file.size ?? file.bytes) !== size)
|
|
230
|
+
throw new Error(
|
|
231
|
+
`${platformId} manifest file size mismatch: ${relative}`,
|
|
232
|
+
);
|
|
233
|
+
const digest = sha256File(absolute);
|
|
234
|
+
const expected = `sha256:${String(file.sha256 || "").replace(/^sha256:/u, "")}`;
|
|
235
|
+
if (digest !== expected)
|
|
236
|
+
throw new Error(
|
|
237
|
+
`${platformId} manifest file digest mismatch: ${relative}`,
|
|
238
|
+
);
|
|
239
|
+
return { path: relative, size, digest };
|
|
240
|
+
})
|
|
241
|
+
.sort((left, right) =>
|
|
242
|
+
Buffer.from(left.path).compare(Buffer.from(right.path)),
|
|
243
|
+
);
|
|
244
|
+
if (files.length === 0)
|
|
245
|
+
throw new Error(`${platformId} manifest has no retained files`);
|
|
246
|
+
const observedArtifactRoot = v4ContentRoot(
|
|
247
|
+
"tail-reseal-artifact-files",
|
|
248
|
+
files,
|
|
249
|
+
);
|
|
250
|
+
const observedManifestRoot = sha256File(manifestPath);
|
|
251
|
+
const retained = mode === "retained";
|
|
252
|
+
if (
|
|
253
|
+
retained &&
|
|
254
|
+
(observedArtifactRoot !== platform.artifactRoot ||
|
|
255
|
+
observedManifestRoot !== platform.manifestRoot)
|
|
256
|
+
)
|
|
257
|
+
throw new Error(
|
|
258
|
+
`${platformId} retained artifact or manifest root mismatch`,
|
|
259
|
+
);
|
|
260
|
+
if (!retained) {
|
|
261
|
+
if (
|
|
262
|
+
observedArtifactRoot === platform.artifactRoot ||
|
|
263
|
+
observedManifestRoot === platform.manifestRoot
|
|
264
|
+
)
|
|
265
|
+
throw new Error(
|
|
266
|
+
"macos-arm64 reseal did not produce a distinct signed manifest",
|
|
267
|
+
);
|
|
268
|
+
if (providerReadbackRoot !== normalized.signing.providerReadbackRoot)
|
|
269
|
+
throw new Error("macos-arm64 provider readback root mismatch");
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
platformId,
|
|
273
|
+
artifactRoot: observedArtifactRoot,
|
|
274
|
+
manifestRoot: observedManifestRoot,
|
|
275
|
+
capsuleRoot: platform.capsuleRoot,
|
|
276
|
+
byteIdentical: retained,
|
|
277
|
+
providerReadbackRoot: retained ? null : providerReadbackRoot,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function collectReadbacks(directory) {
|
|
282
|
+
const byPlatform = new Map();
|
|
283
|
+
for (const entry of fs.readdirSync(path.resolve(directory), {
|
|
284
|
+
recursive: true,
|
|
285
|
+
withFileTypes: true,
|
|
286
|
+
})) {
|
|
287
|
+
if (!entry.isFile() || !entry.name.endsWith("readback.json")) continue;
|
|
288
|
+
const value = readJson(path.join(entry.parentPath, entry.name));
|
|
289
|
+
if (byPlatform.has(value.platformId))
|
|
290
|
+
throw new Error(`duplicate tail reseal readback for ${value.platformId}`);
|
|
291
|
+
byPlatform.set(value.platformId, value);
|
|
292
|
+
}
|
|
293
|
+
return V4_TAIL_RESEAL_PLATFORMS.map((platformId) => {
|
|
294
|
+
if (!byPlatform.has(platformId))
|
|
295
|
+
throw new Error(`missing tail reseal readback for ${platformId}`);
|
|
296
|
+
return byPlatform.get(platformId);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function runV4TailResealCli(args = process.argv.slice(2)) {
|
|
301
|
+
const [command, ...options] = args;
|
|
302
|
+
const requestPath = flag(options, "request");
|
|
303
|
+
if (command === "plan") {
|
|
304
|
+
const request = normalizeV4TailResealRequest(
|
|
305
|
+
readJson(required(requestPath, "--request"), "tail reseal request"),
|
|
306
|
+
);
|
|
307
|
+
const plan = planV4TailReseal(request);
|
|
308
|
+
const output = writeJson(
|
|
309
|
+
flag(options, "output", ".buildchain/tail-reseal/plan.json"),
|
|
310
|
+
plan,
|
|
311
|
+
);
|
|
312
|
+
appendOutputs({
|
|
313
|
+
"plan-root": plan.planRoot,
|
|
314
|
+
"plan-path": path.relative(process.cwd(), output),
|
|
315
|
+
"source-run-id": request.source.runId,
|
|
316
|
+
"source-run-attempt": request.source.runAttempt,
|
|
317
|
+
"source-sha": request.source.sha,
|
|
318
|
+
"source-tree-sha": request.source.treeSha,
|
|
319
|
+
"runtime-sha": request.runtime.sha,
|
|
320
|
+
"consumer-policy-receipt-root": request.runtime.consumerPolicyReceiptRoot,
|
|
321
|
+
"warrant-readback-root": request.warrant.stateReadbackRoot,
|
|
322
|
+
"signing-provider-readback-root": request.signing.providerReadbackRoot,
|
|
323
|
+
"release-tail-provider-readback-root":
|
|
324
|
+
request.releaseTail.providerReadbackRoot,
|
|
325
|
+
"signing-authority-repository": request.signing.authorityRepository,
|
|
326
|
+
"signing-authority-run-id": request.signing.authorityRunId,
|
|
327
|
+
"signing-result-artifact": request.signing.resultArtifact,
|
|
328
|
+
"target-version": request.target.version,
|
|
329
|
+
"platforms-json": JSON.stringify(request.platforms),
|
|
330
|
+
});
|
|
331
|
+
return plan;
|
|
332
|
+
}
|
|
333
|
+
if (command === "admit") {
|
|
334
|
+
const request = normalizeV4TailResealRequest(
|
|
335
|
+
readJson(required(requestPath, "--request"), "tail reseal request"),
|
|
336
|
+
);
|
|
337
|
+
return admitFromGitHub(request).then((admission) => {
|
|
338
|
+
const output = writeJson(
|
|
339
|
+
flag(options, "output", ".buildchain/tail-reseal/admission.json"),
|
|
340
|
+
admission,
|
|
341
|
+
);
|
|
342
|
+
appendOutputs({
|
|
343
|
+
"admission-root": admission.admissionRoot,
|
|
344
|
+
"admission-path": path.relative(process.cwd(), output),
|
|
345
|
+
});
|
|
346
|
+
return admission;
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (command === "verify-platform") {
|
|
350
|
+
const readback = verifyV4TailResealPlatform({
|
|
351
|
+
request: readJson(required(requestPath, "--request")),
|
|
352
|
+
platformId: required(flag(options, "platform"), "--platform"),
|
|
353
|
+
artifactRoot: flag(options, "artifact-root", process.cwd()),
|
|
354
|
+
mode: flag(options, "mode", "retained"),
|
|
355
|
+
providerReadbackRoot: flag(options, "provider-readback-root") || null,
|
|
356
|
+
});
|
|
357
|
+
const output = writeJson(
|
|
358
|
+
flag(
|
|
359
|
+
options,
|
|
360
|
+
"output",
|
|
361
|
+
`.buildchain/tail-reseal/${readback.platformId}-readback.json`,
|
|
362
|
+
),
|
|
363
|
+
readback,
|
|
364
|
+
);
|
|
365
|
+
appendOutputs({
|
|
366
|
+
"readback-path": path.relative(process.cwd(), output),
|
|
367
|
+
"artifact-root": readback.artifactRoot,
|
|
368
|
+
"manifest-root": readback.manifestRoot,
|
|
369
|
+
});
|
|
370
|
+
return readback;
|
|
371
|
+
}
|
|
372
|
+
if (command === "seal") {
|
|
373
|
+
const request = readJson(required(requestPath, "--request"));
|
|
374
|
+
const receipt = createV4TailResealReceipt({
|
|
375
|
+
request,
|
|
376
|
+
plan: readJson(required(flag(options, "plan"), "--plan")),
|
|
377
|
+
readbacks: collectReadbacks(
|
|
378
|
+
required(flag(options, "readbacks"), "--readbacks"),
|
|
379
|
+
),
|
|
380
|
+
passport: readJson(required(flag(options, "passport"), "--passport")),
|
|
381
|
+
protectedReadbackRoot: required(
|
|
382
|
+
flag(options, "protected-readback-root"),
|
|
383
|
+
"--protected-readback-root",
|
|
384
|
+
),
|
|
385
|
+
currentRun: {
|
|
386
|
+
id: Number(required(process.env.GITHUB_RUN_ID, "GITHUB_RUN_ID")),
|
|
387
|
+
attempt: Number(
|
|
388
|
+
required(process.env.GITHUB_RUN_ATTEMPT, "GITHUB_RUN_ATTEMPT"),
|
|
389
|
+
),
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
const output = writeJson(
|
|
393
|
+
flag(options, "output", ".buildchain/tail-reseal/receipt.json"),
|
|
394
|
+
receipt,
|
|
395
|
+
);
|
|
396
|
+
appendOutputs({
|
|
397
|
+
"receipt-root": receipt.receiptRoot,
|
|
398
|
+
"receipt-path": path.relative(process.cwd(), output),
|
|
399
|
+
});
|
|
400
|
+
return receipt;
|
|
401
|
+
}
|
|
402
|
+
throw new Error(
|
|
403
|
+
"usage: buildchain tail-reseal <plan|admit|verify-platform|seal> --request <request.json> [options]",
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (
|
|
408
|
+
!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT &&
|
|
409
|
+
process.argv[1] &&
|
|
410
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
411
|
+
) {
|
|
412
|
+
runV4TailResealCli().catch((error) => {
|
|
413
|
+
console.error(`tail-reseal: ${error.message}`);
|
|
414
|
+
process.exitCode = 1;
|
|
415
|
+
});
|
|
416
|
+
}
|