@kungfu-tech/buildchain 3.0.4-alpha.0 → 3.0.4-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/site/buildchain-contract.json +6 -6
- package/dist/site/buildchain-site.json +4 -4
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/node-api-registry.json +1 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +4 -4
- package/package.json +1 -1
- package/packages/core/paper-fleet.js +8 -3
- package/packages/core/paper.js +12 -5
- package/scripts/aggregate-build-summary.mjs +67 -4
- package/scripts/auditable-demo-renditions.mjs +131 -0
- package/scripts/auditable-demo.mjs +88 -91
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"product": {
|
|
5
5
|
"name": "Buildchain",
|
|
6
6
|
"package": "@kungfu-tech/buildchain",
|
|
7
|
-
"version": "3.0.4-alpha.
|
|
7
|
+
"version": "3.0.4-alpha.2",
|
|
8
8
|
"repository": "https://github.com/kungfu-systems/buildchain"
|
|
9
9
|
},
|
|
10
10
|
"majorLine": "v3",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"retryable GitHub fallback fetches use a bounded attempt budget before exact source SHA and tree verification"
|
|
53
53
|
],
|
|
54
54
|
"breakingDigest": "sha256:dfbedd543ae0c067982b5c8c02d98dd2f7f7df88752be97fb330da6b0ba30faa",
|
|
55
|
-
"auditDigest": "sha256:
|
|
55
|
+
"auditDigest": "sha256:5ae0793e1da87242a56d7fdf29a2d1d7d24ca50f68429797a7d78bcba605b797"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"contractVersion": 1,
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
"media qualification does not claim browser playback, responsive layout, reduced-motion behavior, accessibility, or production deployment"
|
|
322
322
|
],
|
|
323
323
|
"breakingDigest": "sha256:425f8d3d1ba205dd6fe35a20f1dc6dad7d7c563872283e3af79fea4d5c475692",
|
|
324
|
-
"auditDigest": "sha256:
|
|
324
|
+
"auditDigest": "sha256:4f0051783d637ff25a1154379dc0eb77a246ad7aee18e12a5e8ff0107aa61a90"
|
|
325
325
|
},
|
|
326
326
|
{
|
|
327
327
|
"contractVersion": 1,
|
|
@@ -731,7 +731,7 @@
|
|
|
731
731
|
"agents can discover supported Node APIs without importing internal file paths"
|
|
732
732
|
],
|
|
733
733
|
"breakingDigest": "sha256:48f925608d3e2131d90936b07dc2a30341204cae3e6e785c0f77d61ad755c945",
|
|
734
|
-
"auditDigest": "sha256:
|
|
734
|
+
"auditDigest": "sha256:db0fe262bcfc34ea78f8b9b36333b93ab9388548a46b4616b2105156b3f1173a"
|
|
735
735
|
},
|
|
736
736
|
{
|
|
737
737
|
"contractVersion": 1,
|
|
@@ -1340,7 +1340,7 @@
|
|
|
1340
1340
|
"missing receipts are non-qualifying and must not be represented as a successful controller run"
|
|
1341
1341
|
],
|
|
1342
1342
|
"breakingDigest": "sha256:6b8d38e92b2c67b5bc83fae0a3fd95bfa47582fbc176e6cdeebe7630a57534cb",
|
|
1343
|
-
"auditDigest": "sha256:
|
|
1343
|
+
"auditDigest": "sha256:5ae0793e1da87242a56d7fdf29a2d1d7d24ca50f68429797a7d78bcba605b797"
|
|
1344
1344
|
},
|
|
1345
1345
|
{
|
|
1346
1346
|
"contractVersion": 1,
|
|
@@ -3522,5 +3522,5 @@
|
|
|
3522
3522
|
}
|
|
3523
3523
|
],
|
|
3524
3524
|
"compatibilityDigest": "sha256:61af7d0871220bc0909008bdc571905ad500cccf853e7841bc5d462a34b2d436",
|
|
3525
|
-
"contractDigest": "sha256:
|
|
3525
|
+
"contractDigest": "sha256:52a759d10606f84efc0e1c25d5eb9afd4ad55835403d6a376a40dadd7f4648c4"
|
|
3526
3526
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-bundle",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-01T01:21:41.151Z",
|
|
5
|
+
"publishedAt": "2026-08-01T01:21:41.151Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "9e775fa1f698d857f8325c9c89777ea07ee98294",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "3.0.4-alpha.
|
|
40
|
+
"version": "3.0.4-alpha.2",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"source": {
|
|
@@ -616,7 +616,7 @@
|
|
|
616
616
|
"specifier": "@kungfu-tech/buildchain/paper",
|
|
617
617
|
"export": "./paper",
|
|
618
618
|
"target": "./packages/core/paper.js",
|
|
619
|
-
"digest": "sha256:
|
|
619
|
+
"digest": "sha256:5ccd8a5d3a8b77573ce1fefd12ff2850740df122aae5156f1eeaa943fc09fefc",
|
|
620
620
|
"summary": "Unified Paper work, fleet, scaffold, preflight, npm bootstrap, build, Alpha, status, and resume planning APIs.",
|
|
621
621
|
"capabilityGroup": "reusable-build",
|
|
622
622
|
"audience": [
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-publication-release-registry",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-01T01:21:41.151Z",
|
|
5
|
+
"publishedAt": "2026-08-01T01:21:41.151Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "9e775fa1f698d857f8325c9c89777ea07ee98294",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"package": {
|
|
34
34
|
"name": "@kungfu-tech/buildchain",
|
|
35
|
-
"version": "3.0.4-alpha.
|
|
35
|
+
"version": "3.0.4-alpha.2",
|
|
36
36
|
"versionSource": "package.json#version"
|
|
37
37
|
},
|
|
38
38
|
"sourceKind": "package-site-bundle",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"contract": "kungfu-buildchain-site-manifest",
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"publishedAt": "2026-08-
|
|
4
|
+
"generatedAt": "2026-08-01T01:21:41.151Z",
|
|
5
|
+
"publishedAt": "2026-08-01T01:21:41.151Z",
|
|
6
6
|
"reproducible": true,
|
|
7
7
|
"timestampPolicy": "ci-injected",
|
|
8
8
|
"deterministicInputs": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"declared Buildchain surface manifest contract"
|
|
20
20
|
],
|
|
21
21
|
"sourceDateEpoch": "0",
|
|
22
|
-
"sourceRevision": "
|
|
22
|
+
"sourceRevision": "9e775fa1f698d857f8325c9c89777ea07ee98294",
|
|
23
23
|
"timestampPolicyDetails": {
|
|
24
24
|
"contract": "kungfu-buildchain-surface-timestamp-policy",
|
|
25
25
|
"timestampFields": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"package": {
|
|
39
39
|
"name": "@kungfu-tech/buildchain",
|
|
40
|
-
"version": "3.0.4-alpha.
|
|
40
|
+
"version": "3.0.4-alpha.2",
|
|
41
41
|
"versionSource": "package.json#version"
|
|
42
42
|
},
|
|
43
43
|
"entrypoint": "buildchain-site.json",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kungfu-tech/buildchain",
|
|
3
|
-
"version": "3.0.4-alpha.
|
|
3
|
+
"version": "3.0.4-alpha.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
|
|
6
6
|
"repository": "https://github.com/kungfu-systems/buildchain",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
planPaperMigration,
|
|
5
|
+
resolvePaperRuntimeGitSha,
|
|
6
|
+
writePaperMigration,
|
|
7
|
+
} from "./paper.js";
|
|
4
8
|
import {
|
|
5
9
|
PAPER_PATHS,
|
|
6
10
|
PAPER_WORK_BRANCH_PATTERN,
|
|
7
|
-
gitValue,
|
|
8
11
|
paperWorkSource,
|
|
9
12
|
readJson,
|
|
10
13
|
rootedPlan,
|
|
@@ -188,7 +191,9 @@ export function collectPaperFleetAudit({
|
|
|
188
191
|
root: resolvedRoot,
|
|
189
192
|
runtime: {
|
|
190
193
|
version: buildchainVersion,
|
|
191
|
-
sha:
|
|
194
|
+
sha:
|
|
195
|
+
buildchainSha ||
|
|
196
|
+
resolvePaperRuntimeGitSha(buildchainRoot, buildchainVersion),
|
|
192
197
|
},
|
|
193
198
|
summary: {
|
|
194
199
|
repositories: entries.length,
|
package/packages/core/paper.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
PAPER_AGENT_ENTRY_CONTRACT,
|
|
36
36
|
collectPaperAgentEntry,
|
|
37
37
|
paperAgentEntryFiles,
|
|
38
|
+
resolvePaperBuildchainSha,
|
|
38
39
|
} from "./paper-agent-entry.js";
|
|
39
40
|
import {
|
|
40
41
|
managedPaperPackageJson,
|
|
@@ -177,8 +178,11 @@ function buildchainPackageIdentity(buildchainRoot, explicitVersion = "") {
|
|
|
177
178
|
};
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
function
|
|
181
|
-
|
|
181
|
+
export function resolvePaperRuntimeGitSha(
|
|
182
|
+
buildchainRoot,
|
|
183
|
+
buildchainVersion = "",
|
|
184
|
+
) {
|
|
185
|
+
const value = resolvePaperBuildchainSha(buildchainRoot);
|
|
182
186
|
if (GIT_SHA_PATTERN.test(value)) return value;
|
|
183
187
|
const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
|
|
184
188
|
if (!identity.version) return "";
|
|
@@ -661,7 +665,8 @@ export function planPaperScaffold({
|
|
|
661
665
|
buildchainVersion,
|
|
662
666
|
);
|
|
663
667
|
const runtimeSha =
|
|
664
|
-
buildchainSha ||
|
|
668
|
+
buildchainSha ||
|
|
669
|
+
resolvePaperRuntimeGitSha(buildchainRoot, runtimeIdentity.version);
|
|
665
670
|
if (!GIT_SHA_PATTERN.test(runtimeSha)) {
|
|
666
671
|
throw new Error(
|
|
667
672
|
"paper scaffold cannot resolve the exact Buildchain runtime SHA; pass buildchainSha or use a published Buildchain package with npm gitHead provenance",
|
|
@@ -854,7 +859,8 @@ function migrationFiles({
|
|
|
854
859
|
"paper publish package",
|
|
855
860
|
);
|
|
856
861
|
const runtimeSha =
|
|
857
|
-
buildchainSha ||
|
|
862
|
+
buildchainSha ||
|
|
863
|
+
resolvePaperRuntimeGitSha(buildchainRoot, buildchainVersion);
|
|
858
864
|
if (!GIT_SHA_PATTERN.test(runtimeSha)) {
|
|
859
865
|
throw new Error("paper migration requires an exact Buildchain source SHA");
|
|
860
866
|
}
|
|
@@ -1734,7 +1740,8 @@ function runtimeFacts({
|
|
|
1734
1740
|
version: identity.version,
|
|
1735
1741
|
ref: buildchainRef,
|
|
1736
1742
|
resolvedSha:
|
|
1737
|
-
buildchainSha ||
|
|
1743
|
+
buildchainSha ||
|
|
1744
|
+
resolvePaperRuntimeGitSha(buildchainRoot, identity.version),
|
|
1738
1745
|
contract: contractWorld.contract,
|
|
1739
1746
|
contractDigest: contractWorld.contractDigest,
|
|
1740
1747
|
compatibilityDigest: contractWorld.compatibilityDigest,
|
|
@@ -8,6 +8,51 @@ function readEnv(name, fallback = "") {
|
|
|
8
8
|
return process.env[name] || fallback;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function parsePlatformIds(name) {
|
|
12
|
+
const raw = readEnv(name, "").trim();
|
|
13
|
+
if (!raw) return [];
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!Array.isArray(parsed)) {
|
|
16
|
+
throw new Error(`${name} must be a JSON array`);
|
|
17
|
+
}
|
|
18
|
+
const ids = parsed.map((entry) => String(
|
|
19
|
+
typeof entry === "string" ? entry : entry?.id || "",
|
|
20
|
+
).trim());
|
|
21
|
+
if (ids.some((id) => !id)) {
|
|
22
|
+
throw new Error(`${name} entries must declare non-empty platform ids`);
|
|
23
|
+
}
|
|
24
|
+
if (new Set(ids).size !== ids.length) {
|
|
25
|
+
throw new Error(`${name} must not contain duplicate platform ids`);
|
|
26
|
+
}
|
|
27
|
+
return ids;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readManifest(file) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function selectPlatformManifests({ inputRoot, expectedPlatformIds }) {
|
|
39
|
+
const candidates = findJsonFiles(inputRoot)
|
|
40
|
+
.filter((file) => path.basename(file) === "manifest.json")
|
|
41
|
+
.sort()
|
|
42
|
+
.map((file) => ({ file, manifest: readManifest(file) }))
|
|
43
|
+
.filter(({ manifest }) => manifest?.contract === "kungfu-buildchain-artifact");
|
|
44
|
+
if (expectedPlatformIds.length === 0) return candidates;
|
|
45
|
+
return expectedPlatformIds.map((platformId) => {
|
|
46
|
+
const matches = candidates.filter(({ manifest }) => manifest.platform?.id === platformId);
|
|
47
|
+
if (matches.length !== 1) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`expected exactly one platform manifest for ${platformId}, found ${matches.length} under ${inputRoot}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return matches[0];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
11
56
|
export function aggregateBuildSummaryCli() {
|
|
12
57
|
const inputRoot = path.resolve(readEnv("BUILDCHAIN_SUMMARY_INPUT", ".buildchain/downloaded-manifests"));
|
|
13
58
|
const outputPath = path.resolve(readEnv("BUILDCHAIN_SUMMARY_OUTPUT", ".buildchain/artifacts/build-summary.json"));
|
|
@@ -18,10 +63,28 @@ export function aggregateBuildSummaryCli() {
|
|
|
18
63
|
throw new Error("BUILDCHAIN_ADDITIONAL_PLATFORM_COUNT must be a non-negative integer");
|
|
19
64
|
}
|
|
20
65
|
const expectedManifestCount = expectedPlatformCount + additionalPlatformCount;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
66
|
+
const platformIds = parsePlatformIds("BUILDCHAIN_EXPECTED_PLATFORMS_JSON");
|
|
67
|
+
const additionalPlatformIds = parsePlatformIds("BUILDCHAIN_ADDITIONAL_PLATFORM_IDS_JSON");
|
|
68
|
+
if (platformIds.length > 0 && platformIds.length !== expectedPlatformCount) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`BUILDCHAIN_EXPECTED_PLATFORMS_JSON declares ${platformIds.length} platforms, expected ${expectedPlatformCount}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (additionalPlatformIds.length !== additionalPlatformCount) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`BUILDCHAIN_ADDITIONAL_PLATFORM_IDS_JSON declares ${additionalPlatformIds.length} platforms, expected ${additionalPlatformCount}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const expectedPlatformIds = [...platformIds, ...additionalPlatformIds];
|
|
79
|
+
if (new Set(expectedPlatformIds).size !== expectedPlatformIds.length) {
|
|
80
|
+
throw new Error("declared platform ids must be unique across primary and additional platforms");
|
|
81
|
+
}
|
|
82
|
+
const selected = selectPlatformManifests({
|
|
83
|
+
inputRoot,
|
|
84
|
+
expectedPlatformIds,
|
|
85
|
+
});
|
|
86
|
+
const manifestFiles = selected.map(({ file }) => file);
|
|
87
|
+
const manifests = selected.map(({ manifest }) => manifest);
|
|
25
88
|
if (expectedManifestCount > 0 && manifests.length !== expectedManifestCount) {
|
|
26
89
|
throw new Error(
|
|
27
90
|
`expected ${expectedManifestCount} platform manifests, found ${manifests.length} under ${inputRoot}`,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const TERMINAL_CAPTURE_NON_AUTHORITIES = [
|
|
5
|
+
"first-party-identity",
|
|
6
|
+
"system-identity",
|
|
7
|
+
"kfd-compliance",
|
|
8
|
+
"product-system-metadata",
|
|
9
|
+
"package-metadata",
|
|
10
|
+
"registry-history",
|
|
11
|
+
"scan-output",
|
|
12
|
+
"standalone-generation",
|
|
13
|
+
];
|
|
14
|
+
const RENDITION_SET_NON_AUTHORITIES = [
|
|
15
|
+
"publication-authority",
|
|
16
|
+
"runtime-authority",
|
|
17
|
+
...TERMINAL_CAPTURE_NON_AUTHORITIES,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function validateTerminalCapture(value, scene, helpers) {
|
|
21
|
+
const { decodeBase64, digestPattern, exactKeys, integer, invariant, maxBytes, maxEvents, text } = helpers;
|
|
22
|
+
exactKeys(
|
|
23
|
+
value,
|
|
24
|
+
["schema", "command", "dimensions", "durationMs", "encoding", "events", "completion", "exitCode", "authority"],
|
|
25
|
+
[],
|
|
26
|
+
"terminalCapture",
|
|
27
|
+
);
|
|
28
|
+
invariant(value.schema === "kungfu.terminal-capture/v1", "unsupported terminal capture schema");
|
|
29
|
+
text(value.command, 1, 160, "terminalCapture.command");
|
|
30
|
+
exactKeys(value.dimensions, ["columns", "rows"], [], "terminalCapture.dimensions");
|
|
31
|
+
integer(value.dimensions.columns, 80, 200, "terminalCapture.dimensions.columns");
|
|
32
|
+
integer(value.dimensions.rows, 24, 80, "terminalCapture.dimensions.rows");
|
|
33
|
+
const durationMs = integer(value.durationMs, 500, 60000, "terminalCapture.durationMs");
|
|
34
|
+
invariant(
|
|
35
|
+
durationMs <= scene.durationMs && scene.durationMs - durationMs <= 2000,
|
|
36
|
+
"terminal capture duration must end within two seconds of the scene",
|
|
37
|
+
);
|
|
38
|
+
invariant(value.encoding === "base64", "terminalCapture.encoding must be base64");
|
|
39
|
+
invariant(
|
|
40
|
+
Array.isArray(value.events) && value.events.length > 0 && value.events.length <= maxEvents,
|
|
41
|
+
`terminalCapture.events must contain 1 through ${maxEvents} events`,
|
|
42
|
+
);
|
|
43
|
+
let previousAtMs = -1;
|
|
44
|
+
let totalBytes = 0;
|
|
45
|
+
for (const [index, event] of value.events.entries()) {
|
|
46
|
+
exactKeys(event, ["atMs", "data"], [], `terminalCapture.events[${index}]`);
|
|
47
|
+
const atMs = integer(event.atMs, 0, durationMs - 1, `terminalCapture.events[${index}].atMs`);
|
|
48
|
+
invariant(atMs >= previousAtMs, "terminal capture event timestamps must be monotonic");
|
|
49
|
+
invariant(index > 0 || atMs === 0, "the first terminal capture event must start at zero");
|
|
50
|
+
previousAtMs = atMs;
|
|
51
|
+
totalBytes += decodeBase64(event.data, `terminalCapture.events[${index}].data`).length;
|
|
52
|
+
invariant(totalBytes <= maxBytes, "terminal capture exceeds the 4 MiB byte bound");
|
|
53
|
+
}
|
|
54
|
+
exactKeys(value.completion, ["schema", "status", "reportRoot", "eventCount"], [], "terminalCapture.completion");
|
|
55
|
+
invariant(
|
|
56
|
+
value.completion.schema === "kungfu.agent-work-lab.tui-autoplay/v1"
|
|
57
|
+
&& value.completion.status === "qualified"
|
|
58
|
+
&& digestPattern.test(value.completion.reportRoot),
|
|
59
|
+
"terminal capture completion sentinel is not a qualified Agent Work Lab autoplay",
|
|
60
|
+
);
|
|
61
|
+
integer(value.completion.eventCount, 1, 100_000, "terminalCapture.completion.eventCount");
|
|
62
|
+
invariant(value.exitCode === 0, "terminal capture exitCode must be zero");
|
|
63
|
+
exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "terminalCapture.authority");
|
|
64
|
+
invariant(value.authority.classification === "volatile-terminal-observation", "terminal capture authority classification must remain observation-only");
|
|
65
|
+
invariant(Array.isArray(value.authority.grants) && value.authority.grants.length === 0, "terminal capture must not grant authority");
|
|
66
|
+
invariant(
|
|
67
|
+
JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(TERMINAL_CAPTURE_NON_AUTHORITIES),
|
|
68
|
+
"terminal capture must declare every identity and metadata non-authority",
|
|
69
|
+
);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function validateRenditionSet(output, helpers) {
|
|
74
|
+
const { decodeUtf8, exactKeys, invariant, maxBytes, readJson, readRegular, sha256, validateProjection, validateScene } = helpers;
|
|
75
|
+
const manifestPath = path.join(output, "rendition-set.json");
|
|
76
|
+
if (!fs.existsSync(manifestPath)) return null;
|
|
77
|
+
const value = readJson(manifestPath, "rendition set");
|
|
78
|
+
exactKeys(value, ["schema", "renditions", "authority"], [], "renditionSet");
|
|
79
|
+
invariant(value.schema === "kungfu.auditable-demo.rendition-set/v1", "unsupported rendition set schema");
|
|
80
|
+
exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "renditionSet.authority");
|
|
81
|
+
invariant(
|
|
82
|
+
value.authority.classification === "capture-routing-metadata"
|
|
83
|
+
&& Array.isArray(value.authority.grants)
|
|
84
|
+
&& value.authority.grants.length === 0
|
|
85
|
+
&& JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(RENDITION_SET_NON_AUTHORITIES),
|
|
86
|
+
"rendition set authority boundary is invalid",
|
|
87
|
+
);
|
|
88
|
+
invariant(Array.isArray(value.renditions) && value.renditions.length === 2, "rendition set must contain exactly two captures");
|
|
89
|
+
const expected = [
|
|
90
|
+
{
|
|
91
|
+
id: "1080p", role: "primary", transcript: "complete-transcript.txt",
|
|
92
|
+
projection: "public-projection.json", scene: "scene.json", terminalCapture: "terminal-capture.json",
|
|
93
|
+
width: 1920, height: 1080,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "720p", role: "responsive", transcript: "complete-transcript-720p.txt",
|
|
97
|
+
projection: "public-projection-720p.json", scene: "scene-720p.json", terminalCapture: "terminal-capture-720p.json",
|
|
98
|
+
width: 1280, height: 720,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
const normalized = value.renditions.map((entry, index) => {
|
|
102
|
+
const label = `renditionSet.renditions[${index}]`;
|
|
103
|
+
exactKeys(entry, ["id", "role", "transcript", "projection", "scene", "terminalCapture", "captureRoot"], [], label);
|
|
104
|
+
const declaration = expected[index];
|
|
105
|
+
for (const key of ["id", "role", "transcript", "projection", "scene", "terminalCapture"]) {
|
|
106
|
+
invariant(entry[key] === declaration[key], `${label}.${key} is not the exact native rendition contract`);
|
|
107
|
+
}
|
|
108
|
+
const transcriptBytes = readRegular(path.join(output, entry.transcript), `${label} transcript`, 4 * 1024 * 1024);
|
|
109
|
+
const transcript = decodeUtf8(transcriptBytes, `${label} transcript`).replace(/\r\n/g, "\n");
|
|
110
|
+
invariant(transcript.trim().length > 0, `${label} transcript must not be empty`);
|
|
111
|
+
const lines = transcript.endsWith("\n") ? transcript.slice(0, -1).split("\n") : transcript.split("\n");
|
|
112
|
+
const scene = validateScene(readJson(path.join(output, entry.scene), `${label} scene`));
|
|
113
|
+
invariant(scene.width === declaration.width && scene.height === declaration.height, `${label} scene dimensions are not native`);
|
|
114
|
+
const projection = validateProjection(readJson(path.join(output, entry.projection), `${label} projection`), scene, lines.length);
|
|
115
|
+
const captureBytes = readRegular(path.join(output, entry.terminalCapture), `${label} terminal capture`, maxBytes);
|
|
116
|
+
const capture = validateTerminalCapture(JSON.parse(decodeUtf8(captureBytes, `${label} terminal capture`)), scene, helpers);
|
|
117
|
+
invariant(entry.captureRoot === sha256(captureBytes), `${label}.captureRoot mismatch`);
|
|
118
|
+
return { ...entry, transcript, lines, scene, projection, capture };
|
|
119
|
+
});
|
|
120
|
+
invariant(normalized[0].captureRoot !== normalized[1].captureRoot, "native rendition capture roots must be distinct");
|
|
121
|
+
invariant(
|
|
122
|
+
JSON.stringify(normalized[0].capture.dimensions) !== JSON.stringify(normalized[1].capture.dimensions),
|
|
123
|
+
"native rendition PTY dimensions must be distinct",
|
|
124
|
+
);
|
|
125
|
+
invariant(
|
|
126
|
+
normalized[0].projection.evidenceClass === normalized[1].projection.evidenceClass
|
|
127
|
+
&& normalized[0].projection.claimBoundary === normalized[1].projection.claimBoundary,
|
|
128
|
+
"native rendition evidence boundaries must match",
|
|
129
|
+
);
|
|
130
|
+
return { schema: value.schema, renditions: normalized, authority: value.authority };
|
|
131
|
+
}
|
|
@@ -7,6 +7,10 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import {
|
|
11
|
+
validateRenditionSet,
|
|
12
|
+
validateTerminalCapture,
|
|
13
|
+
} from "./auditable-demo-renditions.mjs";
|
|
10
14
|
|
|
11
15
|
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
12
16
|
const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
|
|
@@ -22,19 +26,16 @@ const REQUIRED_ADAPTER_FILES = [
|
|
|
22
26
|
"public-projection.json",
|
|
23
27
|
"scene.json",
|
|
24
28
|
];
|
|
25
|
-
const OPTIONAL_ADAPTER_FILES = [
|
|
29
|
+
const OPTIONAL_ADAPTER_FILES = [
|
|
30
|
+
"terminal-capture.json",
|
|
31
|
+
"complete-transcript-720p.txt",
|
|
32
|
+
"public-projection-720p.json",
|
|
33
|
+
"scene-720p.json",
|
|
34
|
+
"terminal-capture-720p.json",
|
|
35
|
+
"rendition-set.json",
|
|
36
|
+
];
|
|
26
37
|
const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
27
38
|
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
|
-
];
|
|
38
39
|
|
|
39
40
|
function invariant(condition, message) {
|
|
40
41
|
if (!condition) throw new Error(message);
|
|
@@ -249,80 +250,11 @@ function validateProjection(value, scene, transcriptLineCount) {
|
|
|
249
250
|
};
|
|
250
251
|
}
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
exactKeys
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
}
|
|
253
|
+
const RENDITION_VALIDATION_HELPERS = {
|
|
254
|
+
decodeBase64, decodeUtf8, digestPattern: DIGEST_PATTERN, exactKeys, integer, invariant,
|
|
255
|
+
maxBytes: MAX_TERMINAL_CAPTURE_BYTES, maxEvents: MAX_TERMINAL_CAPTURE_EVENTS,
|
|
256
|
+
readJson, readRegular, sha256, text, validateProjection, validateScene,
|
|
257
|
+
};
|
|
326
258
|
|
|
327
259
|
function validateSourceCoordinate(value) {
|
|
328
260
|
exactKeys(
|
|
@@ -379,8 +311,17 @@ function validateAdapterOutput(output, strict = true) {
|
|
|
379
311
|
);
|
|
380
312
|
const terminalCapturePath = path.join(output, "terminal-capture.json");
|
|
381
313
|
const terminalCapture = fs.existsSync(terminalCapturePath)
|
|
382
|
-
? validateTerminalCapture(
|
|
314
|
+
? validateTerminalCapture(
|
|
315
|
+
readJson(terminalCapturePath, "terminal capture"),
|
|
316
|
+
scene,
|
|
317
|
+
RENDITION_VALIDATION_HELPERS,
|
|
318
|
+
)
|
|
383
319
|
: null;
|
|
320
|
+
const renditionSet = validateRenditionSet(output, RENDITION_VALIDATION_HELPERS);
|
|
321
|
+
invariant(
|
|
322
|
+
!renditionSet || terminalCapture,
|
|
323
|
+
"rendition set requires the primary terminal capture",
|
|
324
|
+
);
|
|
384
325
|
if (strict) {
|
|
385
326
|
const allowed = new Set([...REQUIRED_ADAPTER_FILES, ...OPTIONAL_ADAPTER_FILES]);
|
|
386
327
|
for (const member of listFiles(output)) invariant(allowed.has(member), `undeclared adapter output: ${member}`);
|
|
@@ -391,6 +332,7 @@ function validateAdapterOutput(output, strict = true) {
|
|
|
391
332
|
scene,
|
|
392
333
|
projection,
|
|
393
334
|
terminalCapture,
|
|
335
|
+
renditionSet,
|
|
394
336
|
};
|
|
395
337
|
}
|
|
396
338
|
|
|
@@ -1047,6 +989,21 @@ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, optio
|
|
|
1047
989
|
const observed = manifest.inputs?.[key]?.root;
|
|
1048
990
|
invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
|
|
1049
991
|
}
|
|
992
|
+
if (expectedInputs.renditionSet) {
|
|
993
|
+
invariant(
|
|
994
|
+
manifest.derivation?.policy === "independent-native-frame-sets/v1",
|
|
995
|
+
"renderer did not use independent native frame sets",
|
|
996
|
+
);
|
|
997
|
+
invariant(
|
|
998
|
+
Array.isArray(manifest.inputs?.renditions)
|
|
999
|
+
&& manifest.inputs.renditions.length === 2
|
|
1000
|
+
&& manifest.inputs.renditions[0]?.role === "primary"
|
|
1001
|
+
&& manifest.inputs.renditions[1]?.role === "responsive"
|
|
1002
|
+
&& manifest.inputs.renditions[0]?.terminalCapture?.root
|
|
1003
|
+
!== manifest.inputs.renditions[1]?.terminalCapture?.root,
|
|
1004
|
+
"renderer native rendition inputs are not independently bound",
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1050
1007
|
const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
|
|
1051
1008
|
invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
|
|
1052
1009
|
const qualification = qualifyRendererOutput(
|
|
@@ -1099,11 +1056,10 @@ function finalizeGate(values) {
|
|
|
1099
1056
|
fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
|
|
1100
1057
|
writeJson(path.join(output, "scene.json"), normalized.scene);
|
|
1101
1058
|
writeJson(path.join(output, "public-projection.json"), normalized.projection);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
path.join(adapterOutput,
|
|
1105
|
-
|
|
1106
|
-
);
|
|
1059
|
+
for (const name of OPTIONAL_ADAPTER_FILES) {
|
|
1060
|
+
if (fs.existsSync(path.join(adapterOutput, name))) {
|
|
1061
|
+
copyFile(path.join(adapterOutput, name), path.join(output, name));
|
|
1062
|
+
}
|
|
1107
1063
|
}
|
|
1108
1064
|
copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
|
|
1109
1065
|
copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
|
|
@@ -1148,6 +1104,22 @@ function finalizeGate(values) {
|
|
|
1148
1104
|
},
|
|
1149
1105
|
}
|
|
1150
1106
|
: {}),
|
|
1107
|
+
...(normalized.renditionSet
|
|
1108
|
+
? {
|
|
1109
|
+
renditionSet: {
|
|
1110
|
+
schema: normalized.renditionSet.schema,
|
|
1111
|
+
root: sha256(readRegular(path.join(output, "rendition-set.json"), "rendition set")),
|
|
1112
|
+
renditions: normalized.renditionSet.renditions.map((rendition) => ({
|
|
1113
|
+
id: rendition.id,
|
|
1114
|
+
role: rendition.role,
|
|
1115
|
+
captureRoot: rendition.captureRoot,
|
|
1116
|
+
sceneRoot: sha256(readRegular(path.join(output, rendition.scene), `${rendition.id} scene`)),
|
|
1117
|
+
transcriptRoot: sha256(readRegular(path.join(output, rendition.transcript), `${rendition.id} transcript`)),
|
|
1118
|
+
projectionRoot: sha256(readRegular(path.join(output, rendition.projection), `${rendition.id} projection`)),
|
|
1119
|
+
})),
|
|
1120
|
+
},
|
|
1121
|
+
}
|
|
1122
|
+
: {}),
|
|
1151
1123
|
evidenceClass: normalized.projection.evidenceClass,
|
|
1152
1124
|
claimBoundary: normalized.projection.claimBoundary,
|
|
1153
1125
|
},
|
|
@@ -1198,6 +1170,27 @@ function verifyGate(values) {
|
|
|
1198
1170
|
"gate terminal capture root mismatch",
|
|
1199
1171
|
);
|
|
1200
1172
|
}
|
|
1173
|
+
const qualifiedRenditionSet = receipt.qualifiedInputs?.renditionSet;
|
|
1174
|
+
invariant(
|
|
1175
|
+
Boolean(qualifiedRenditionSet) === Boolean(normalized.renditionSet),
|
|
1176
|
+
"gate rendition set presence drifted",
|
|
1177
|
+
);
|
|
1178
|
+
if (normalized.renditionSet) {
|
|
1179
|
+
invariant(
|
|
1180
|
+
qualifiedRenditionSet.schema === normalized.renditionSet.schema
|
|
1181
|
+
&& qualifiedRenditionSet.root === sha256(readRegular(path.join(bundle, "rendition-set.json"), "rendition set"))
|
|
1182
|
+
&& JSON.stringify(qualifiedRenditionSet.renditions)
|
|
1183
|
+
=== JSON.stringify(normalized.renditionSet.renditions.map((rendition) => ({
|
|
1184
|
+
id: rendition.id,
|
|
1185
|
+
role: rendition.role,
|
|
1186
|
+
captureRoot: rendition.captureRoot,
|
|
1187
|
+
sceneRoot: sha256(readRegular(path.join(bundle, rendition.scene), `${rendition.id} scene`)),
|
|
1188
|
+
transcriptRoot: sha256(readRegular(path.join(bundle, rendition.transcript), `${rendition.id} transcript`)),
|
|
1189
|
+
projectionRoot: sha256(readRegular(path.join(bundle, rendition.projection), `${rendition.id} projection`)),
|
|
1190
|
+
}))),
|
|
1191
|
+
"gate native rendition roots mismatch",
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1201
1194
|
invariant(receipt.qualifiedInputs.evidenceClass === normalized.projection.evidenceClass, "gate evidence class drifted");
|
|
1202
1195
|
}
|
|
1203
1196
|
|
|
@@ -1228,6 +1221,7 @@ function finalizeMedia(values) {
|
|
|
1228
1221
|
"--media-profile": mediaProfile,
|
|
1229
1222
|
});
|
|
1230
1223
|
const terminalCapturePath = path.join(gateBundle, "terminal-capture.json");
|
|
1224
|
+
const renditionSetPath = path.join(gateBundle, "rendition-set.json");
|
|
1231
1225
|
const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
|
|
1232
1226
|
scene: path.join(gateBundle, "scene.json"),
|
|
1233
1227
|
transcript: path.join(gateBundle, "complete-transcript.txt"),
|
|
@@ -1235,6 +1229,9 @@ function finalizeMedia(values) {
|
|
|
1235
1229
|
...(fs.existsSync(terminalCapturePath)
|
|
1236
1230
|
? { terminalCapture: terminalCapturePath }
|
|
1237
1231
|
: {}),
|
|
1232
|
+
...(fs.existsSync(renditionSetPath)
|
|
1233
|
+
? { renditionSet: renditionSetPath }
|
|
1234
|
+
: {}),
|
|
1238
1235
|
}, {
|
|
1239
1236
|
mediaProfile,
|
|
1240
1237
|
inspectMedia: mediaInspection?.inspectMedia,
|