@xemahq/edition-tooling 0.0.0-stage → 0.1.1

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 (219) hide show
  1. package/LICENSE +175 -0
  2. package/dist/cli.d.ts +7 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +47 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/edition.d.ts +4 -0
  7. package/dist/commands/edition.d.ts.map +1 -0
  8. package/dist/commands/edition.js +58 -0
  9. package/dist/commands/edition.js.map +1 -0
  10. package/dist/edition/verify-image-evidence.bundle.mjs +72104 -0
  11. package/dist/edition/verify-image-evidence.bundle.provenance.json +4119 -0
  12. package/dist/lib/exit-codes.d.ts +6 -0
  13. package/dist/lib/exit-codes.d.ts.map +1 -0
  14. package/dist/lib/exit-codes.js +21 -0
  15. package/dist/lib/exit-codes.js.map +1 -0
  16. package/dist/runtime/edition/run-entry.d.ts +10 -0
  17. package/dist/runtime/edition/run-entry.d.ts.map +1 -0
  18. package/dist/runtime/edition/run-entry.js +49 -0
  19. package/dist/runtime/edition/run-entry.js.map +1 -0
  20. package/dist/runtime/edition/verb-map.d.ts +70 -0
  21. package/dist/runtime/edition/verb-map.d.ts.map +1 -0
  22. package/dist/runtime/edition/verb-map.js +166 -0
  23. package/dist/runtime/edition/verb-map.js.map +1 -0
  24. package/edition/README.md +291 -0
  25. package/edition/adapter-bundled-versions.mjs +128 -0
  26. package/edition/adapter-host-versions.mjs +76 -0
  27. package/edition/assemble-producer-input.mjs +1423 -0
  28. package/edition/assemble-third-party-input.mjs +348 -0
  29. package/edition/authored-source-check.mjs +141 -0
  30. package/edition/authority-binding.mjs +23 -0
  31. package/edition/authorization-base.mjs +357 -0
  32. package/edition/biome-content.mjs +327 -0
  33. package/edition/build-biome-index.mjs +180 -0
  34. package/edition/build-ledger-completeness.mjs +67 -0
  35. package/edition/build-ledger.mjs +107 -0
  36. package/edition/build-platform-service-index.mjs +292 -0
  37. package/edition/build-runtime-image-source-index.mjs +1197 -0
  38. package/edition/built-verifier.mjs +55 -0
  39. package/edition/candidate-edition.mjs +735 -0
  40. package/edition/canonical-evidence-json.mjs +82 -0
  41. package/edition/canonical-repository-url.mjs +61 -0
  42. package/edition/catalogue-edition.mjs +49 -0
  43. package/edition/catalogue-evidence.mjs +228 -0
  44. package/edition/catalogue-fetch.mjs +124 -0
  45. package/edition/catalogue-lock-field.mjs +41 -0
  46. package/edition/catalogue-pin.mjs +71 -0
  47. package/edition/catalogue.mjs +237 -0
  48. package/edition/chart-set-source-commit.mjs +97 -0
  49. package/edition/check-committed-image-evidence.mjs +412 -0
  50. package/edition/checkout-indexed-source-repositories.mjs +283 -0
  51. package/edition/collected-refusals.mjs +59 -0
  52. package/edition/comment-blanker.mjs +75 -0
  53. package/edition/connector-source-authority.mjs +60 -0
  54. package/edition/contract-authority.mjs +122 -0
  55. package/edition/cosign-certificate-constraints.mjs +50 -0
  56. package/edition/cross-repo-build-inputs.mjs +158 -0
  57. package/edition/customer-definition.mjs +72 -0
  58. package/edition/customer-edition-verbs.mjs +610 -0
  59. package/edition/customer-flags.mjs +15 -0
  60. package/edition/definition-checkout.mjs +96 -0
  61. package/edition/develop-build-record.mjs +122 -0
  62. package/edition/develop-catalogue-free-resolve.mjs +282 -0
  63. package/edition/develop-catalogue-free-vendor.mjs +118 -0
  64. package/edition/develop-channel-evidence.mjs +23 -0
  65. package/edition/develop-channel-record.mjs +49 -0
  66. package/edition/develop-channel-satellites.mjs +65 -0
  67. package/edition/develop-chart-roster.mjs +96 -0
  68. package/edition/develop-chart-set.mjs +147 -0
  69. package/edition/develop-current-resolution.mjs +29 -0
  70. package/edition/develop-edition-invariants.mjs +142 -0
  71. package/edition/develop-edition-resolve.mjs +250 -0
  72. package/edition/develop-edition.mjs +332 -0
  73. package/edition/develop-engine-image.mjs +97 -0
  74. package/edition/develop-freeze.mjs +304 -0
  75. package/edition/develop-git-bundles.mjs +296 -0
  76. package/edition/develop-graph.mjs +34 -0
  77. package/edition/develop-image-coordinate.mjs +85 -0
  78. package/edition/develop-image-probe.mjs +122 -0
  79. package/edition/develop-image-record.mjs +260 -0
  80. package/edition/develop-resolve-report.mjs +159 -0
  81. package/edition/develop-select-records.mjs +90 -0
  82. package/edition/develop-selection-line.mjs +29 -0
  83. package/edition/develop-selection-pins.mjs +36 -0
  84. package/edition/develop-snapshot-inputs.mjs +112 -0
  85. package/edition/develop-store-root.mjs +20 -0
  86. package/edition/dispatched-runtime-components.mjs +172 -0
  87. package/edition/dist-check.mjs +1174 -0
  88. package/edition/distribution-evidence-set.mjs +1064 -0
  89. package/edition/distribution-paths.mjs +191 -0
  90. package/edition/edition-artifact.mjs +529 -0
  91. package/edition/edition-biome-manifests.mjs +85 -0
  92. package/edition/edition-ceiling.mjs +182 -0
  93. package/edition/edition-channel.mjs +27 -0
  94. package/edition/edition-chart-set.mjs +104 -0
  95. package/edition/edition-config.mjs +374 -0
  96. package/edition/edition-constants.mjs +110 -0
  97. package/edition/edition-contract-parse.mjs +179 -0
  98. package/edition/edition-contract.json +100 -0
  99. package/edition/edition-image-seal.mjs +115 -0
  100. package/edition/edition-kind.mjs +10 -0
  101. package/edition/edition-list.mjs +35 -0
  102. package/edition/edition-locks.mjs +78 -0
  103. package/edition/edition-pin-verbs.mjs +183 -0
  104. package/edition/edition-pin.mjs +17 -0
  105. package/edition/edition-pinned-inputs.mjs +116 -0
  106. package/edition/edition-protocol.mjs +285 -0
  107. package/edition/edition-publisher.mjs +21 -0
  108. package/edition/edition-seal.mjs +257 -0
  109. package/edition/edition-source-pins.mjs +211 -0
  110. package/edition/edition-unpin.mjs +16 -0
  111. package/edition/edition-workspace.mjs +205 -0
  112. package/edition/evidence-checkout.mjs +439 -0
  113. package/edition/evidence-grammars.mjs +18 -0
  114. package/edition/evidence-input-mode.mjs +136 -0
  115. package/edition/evidence-trust-pin.mjs +392 -0
  116. package/edition/evidence-trust-policy.mjs +374 -0
  117. package/edition/external-source-walk.mjs +486 -0
  118. package/edition/first-party-source-ids.mjs +62 -0
  119. package/edition/fleet-coordinate-finality.mjs +363 -0
  120. package/edition/fulcio-certificate-extensions.mjs +278 -0
  121. package/edition/git-json.mjs +54 -0
  122. package/edition/git-source-auth.mjs +131 -0
  123. package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
  124. package/edition/image-evidence-replay-state.mjs +242 -0
  125. package/edition/image-evidence.mjs +1328 -0
  126. package/edition/image-lock-service-rows.mjs +26 -0
  127. package/edition/image-subject-identity.mjs +19 -0
  128. package/edition/images-from-record.mjs +216 -0
  129. package/edition/indexes-at-pins.mjs +13 -0
  130. package/edition/infra-images-lock.mjs +105 -0
  131. package/edition/kernel-capability-domain.mjs +30 -0
  132. package/edition/kernel-contract-authority.mjs +526 -0
  133. package/edition/kernel-contract-source-path-set.mjs +96 -0
  134. package/edition/kernel-package.mjs +237 -0
  135. package/edition/kernel-pin-source.mjs +63 -0
  136. package/edition/kernel-protocol-choice.mjs +239 -0
  137. package/edition/kernel-signer-kind.mjs +23 -0
  138. package/edition/lib.mjs +3322 -0
  139. package/edition/locked-bundle-signer.mjs +297 -0
  140. package/edition/map-with-concurrency.mjs +19 -0
  141. package/edition/no-follow-file.mjs +94 -0
  142. package/edition/oci-evidence-verbs.mjs +421 -0
  143. package/edition/oci-image-source-evidence.mjs +377 -0
  144. package/edition/operation-grant-admission.mjs +285 -0
  145. package/edition/operation-requirements.mjs +282 -0
  146. package/edition/oras-process.mjs +35 -0
  147. package/edition/oras.mjs +664 -0
  148. package/edition/pin-source.mjs +242 -0
  149. package/edition/pinned-cosign.mjs +86 -0
  150. package/edition/plan-image-evidence-refresh.mjs +428 -0
  151. package/edition/platform-lock-readability.mjs +903 -0
  152. package/edition/platform-service-class.mjs +85 -0
  153. package/edition/platform-service-ownership.mjs +91 -0
  154. package/edition/platform-service-source-authority.mjs +105 -0
  155. package/edition/positive-closure.mjs +162 -0
  156. package/edition/previous-image-lock.mjs +25 -0
  157. package/edition/produce-distribution-completeness-evidence.mjs +317 -0
  158. package/edition/produce-edition-catalogue-evidence.mjs +252 -0
  159. package/edition/produce-image-delivery-entitlement-evidence.mjs +302 -0
  160. package/edition/produce-image-source-evidence.mjs +788 -0
  161. package/edition/producer-evidence-bundle.mjs +201 -0
  162. package/edition/producer-evidence-index-file.mjs +4 -0
  163. package/edition/record-refused.mjs +11 -0
  164. package/edition/registry-client-factory.mjs +21 -0
  165. package/edition/registry-credentials.mjs +225 -0
  166. package/edition/release-catalogue.mjs +48 -0
  167. package/edition/release-channel-guard.mjs +108 -0
  168. package/edition/release-edition-artifact.mjs +152 -0
  169. package/edition/release-edition.mjs +54 -0
  170. package/edition/remedy-command.mjs +89 -0
  171. package/edition/repin-evidence-trust.mjs +109 -0
  172. package/edition/required-resource-provider.mjs +58 -0
  173. package/edition/resolve-chart-set.mjs +376 -0
  174. package/edition/resolve-customer-edition.mjs +374 -0
  175. package/edition/resolve-distribution.mjs +2642 -0
  176. package/edition/resolve-edition.mjs +192 -0
  177. package/edition/resolve-images.mjs +2344 -0
  178. package/edition/resolve-infra-images.mjs +88 -0
  179. package/edition/seal-image-digests.mjs +954 -0
  180. package/edition/sealed-distribution-lock.mjs +22 -0
  181. package/edition/sealed-evidence-accept.mjs +82 -0
  182. package/edition/shallow-checkout.mjs +18 -0
  183. package/edition/sigstore-bundle-media-type.mjs +4 -0
  184. package/edition/source-authority.mjs +713 -0
  185. package/edition/source-closure.mjs +115 -0
  186. package/edition/store-publisher-trust.mjs +40 -0
  187. package/edition/strict-json.mjs +121 -0
  188. package/edition/string-order.mjs +5 -0
  189. package/edition/third-party-admission.mjs +118 -0
  190. package/edition/third-party-bundle-registry.mjs +349 -0
  191. package/edition/third-party-certificate-extensions.mjs +58 -0
  192. package/edition/third-party-image-pins.mjs +41 -0
  193. package/edition/third-party-pins-trusted.mjs +24 -0
  194. package/edition/third-party-producer-evidence.mjs +76 -0
  195. package/edition/third-party-source-check.mjs +28 -0
  196. package/edition/third-party-source-context.mjs +41 -0
  197. package/edition/third-party-source-evidence.mjs +326 -0
  198. package/edition/third-party-source-flags.mjs +148 -0
  199. package/edition/third-party-source-pins.mjs +270 -0
  200. package/edition/third-party-source-uniqueness.mjs +54 -0
  201. package/edition/third-party-source-walk.mjs +118 -0
  202. package/edition/third-party-source-writer.mjs +228 -0
  203. package/edition/third-party-subjects.mjs +128 -0
  204. package/edition/trust-policy-arming-verb.mjs +60 -0
  205. package/edition/trust-policy-arming.mjs +144 -0
  206. package/edition/trust-policy-image-ownership.mjs +85 -0
  207. package/edition/trusted-executable.mjs +166 -0
  208. package/edition/verifier-binding.mjs +45 -0
  209. package/edition/verifier-digest-pin.mjs +17 -0
  210. package/edition/verifier-files.mjs +8 -0
  211. package/edition/verifier-kernel-schemas.mjs +22 -0
  212. package/edition/verifier-provenance.mjs +64 -0
  213. package/edition/verify-catalogue-evidence.mjs +247 -0
  214. package/edition/verify-image-evidence.mjs +493 -0
  215. package/edition/verify-image-platforms.mjs +523 -0
  216. package/edition/why.mjs +56 -0
  217. package/edition/workspace-sections.mjs +287 -0
  218. package/package.json +83 -4
  219. package/README.md +0 -3
@@ -0,0 +1,526 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { lstatSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ import { canonicalJson } from './canonical-evidence-json.mjs';
6
+ import { sameRepository } from './canonical-repository-url.mjs';
7
+ import { kernelRepoDir } from './edition-workspace.mjs';
8
+ import { COMMIT_PATTERN } from './evidence-grammars.mjs';
9
+ import {
10
+ assertSourcePathsEqualSet,
11
+ KERNEL_CONTRACT_SOURCE_PATH_SET_FILE,
12
+ parseKernelContractSourcePathSet,
13
+ } from './kernel-contract-source-path-set.mjs';
14
+
15
+ const GIT_ENV = Object.freeze({
16
+ GIT_CONFIG_NOSYSTEM: '1',
17
+ GIT_CONFIG_GLOBAL: '/dev/null',
18
+ GIT_TERMINAL_PROMPT: '0',
19
+ GIT_NO_REPLACE_OBJECTS: '1',
20
+ GIT_OPTIONAL_LOCKS: '0',
21
+ GIT_PAGER: 'cat',
22
+ LC_ALL: 'C',
23
+ PATH: '/usr/bin:/bin',
24
+ });
25
+ const GIT_SAFE_CONFIG = Object.freeze([
26
+ '-c',
27
+ 'core.fsmonitor=false',
28
+ '-c',
29
+ 'core.hooksPath=/dev/null',
30
+ '-c',
31
+ 'core.attributesFile=/dev/null',
32
+ '--no-pager',
33
+ ]);
34
+ const GIT_TREE_RECORD_PATTERN =
35
+ /^(100644|100755|120000|160000) (blob|commit) ([a-f0-9]{40,64})\t(.+)$/;
36
+
37
+ /** The kernel authority repository may be the whole of xema-base: `ls-files` of its tree exceeds the 1 MiB default. */
38
+ const GIT_MAX_BUFFER = 1024 * 1024 * 1024;
39
+
40
+ function git(repositoryDirectory, args, encoding = 'utf8') {
41
+ return execFileSync(
42
+ '/usr/bin/git',
43
+ [...GIT_SAFE_CONFIG, '-C', repositoryDirectory, ...args],
44
+ {
45
+ encoding,
46
+ stdio: ['ignore', 'pipe', 'pipe'],
47
+ env: GIT_ENV,
48
+ maxBuffer: GIT_MAX_BUFFER,
49
+ },
50
+ );
51
+ }
52
+
53
+ function exactHeadCommit(repositoryDirectory) {
54
+ const commit = git(repositoryDirectory, [
55
+ 'rev-parse',
56
+ '--verify',
57
+ 'HEAD^{commit}',
58
+ ]).trim();
59
+ if (!COMMIT_PATTERN.test(commit)) {
60
+ throw new Error('kernel contract repository HEAD is not an exact commit');
61
+ }
62
+ return commit;
63
+ }
64
+
65
+ /** True when `ancestor` is reachable from `descendant`. Never throws. */
66
+ function isAncestor(repositoryDirectory, ancestor, descendant) {
67
+ try {
68
+ git(repositoryDirectory, ['merge-base', '--is-ancestor', ancestor, descendant]);
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ function assertCommitExists(repositoryDirectory, sourceCommit) {
76
+ if (!COMMIT_PATTERN.test(sourceCommit)) {
77
+ throw new Error(
78
+ `kernel contract authority has invalid source commit "${String(sourceCommit)}"`,
79
+ );
80
+ }
81
+ try {
82
+ git(repositoryDirectory, ['cat-file', '-e', `${sourceCommit}^{commit}`]);
83
+ } catch {
84
+ throw new Error(
85
+ `kernel contract authority commit ${sourceCommit} is absent from the local Git object database`,
86
+ );
87
+ }
88
+ }
89
+
90
+ function sameRepositoryOrigin(repositoryDirectory, schemas) {
91
+ const origin = git(repositoryDirectory, [
92
+ 'remote',
93
+ 'get-url',
94
+ 'origin',
95
+ ]).trim();
96
+ if (
97
+ !sameRepository(
98
+ origin,
99
+ schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL,
100
+ 'kernel contract repository origin',
101
+ )
102
+ ) {
103
+ throw new Error(
104
+ `kernel contract repository origin must be the repository ${schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL}`,
105
+ );
106
+ }
107
+ return origin;
108
+ }
109
+
110
+ /**
111
+ * The blob record of every tree entry at `sourceCommit`, by path: ONE
112
+ * `ls-tree -r`, however many authority sources there are. A path absent from
113
+ * the map is absent from the commit or is not a blob.
114
+ */
115
+ function gitTreeAtCommit(repositoryDirectory, sourceCommit) {
116
+ const records = execFileSync(
117
+ '/usr/bin/git',
118
+ [...GIT_SAFE_CONFIG, '-C', repositoryDirectory, 'ls-tree', '-r', '-z', sourceCommit],
119
+ { encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], env: GIT_ENV, maxBuffer: GIT_MAX_BUFFER },
120
+ )
121
+ .toString('utf8')
122
+ .split('\0')
123
+ .filter(Boolean);
124
+ const tree = new Map();
125
+ for (const record of records) {
126
+ const match = GIT_TREE_RECORD_PATTERN.exec(record);
127
+ if (match) {tree.set(match[4], { mode: match[1], type: match[2], objectId: match[3] });}
128
+ }
129
+ return tree;
130
+ }
131
+
132
+ function missingSource(expectedPath) {
133
+ return new Error(
134
+ `kernel contract authority source "${expectedPath}" is missing or is not an exact Git blob`,
135
+ );
136
+ }
137
+
138
+ /**
139
+ * `{path, mode, objectId}` of each of `paths` at `sourceCommit`, in order.
140
+ * Refuses a path that is missing or is not a blob.
141
+ */
142
+ function gitBlobRecordsAtCommit(repositoryDirectory, sourceCommit, paths) {
143
+ const tree = gitTreeAtCommit(repositoryDirectory, sourceCommit);
144
+ return paths.map((path) => {
145
+ const entry = tree.get(path);
146
+ if (entry === undefined || entry.type !== 'blob') {throw missingSource(path);}
147
+ return { path, mode: entry.mode, objectId: entry.objectId };
148
+ });
149
+ }
150
+
151
+ /**
152
+ * {@link gitBlobRecordsAtCommit} with each blob's exact bytes, read by ONE
153
+ * `cat-file --batch` and matched to the object id it was asked for.
154
+ */
155
+ function gitBlobsAtCommit(repositoryDirectory, sourceCommit, paths) {
156
+ const records = gitBlobRecordsAtCommit(repositoryDirectory, sourceCommit, paths);
157
+ if (records.length === 0) {return [];}
158
+ const output = execFileSync(
159
+ '/usr/bin/git',
160
+ [...GIT_SAFE_CONFIG, '-C', repositoryDirectory, 'cat-file', '--batch'],
161
+ {
162
+ input: Buffer.from(records.map((record) => `${record.objectId}\n`).join(''), 'utf8'),
163
+ encoding: 'buffer',
164
+ stdio: ['pipe', 'pipe', 'pipe'],
165
+ env: GIT_ENV,
166
+ maxBuffer: GIT_MAX_BUFFER,
167
+ },
168
+ );
169
+ let offset = 0;
170
+ const sources = records.map((record) => {
171
+ const headerEnd = output.indexOf(0x0a, offset);
172
+ const header = headerEnd === -1 ? '' : output.subarray(offset, headerEnd).toString('utf8');
173
+ const match = /^([a-f0-9]{40,64}) blob (0|[1-9][0-9]*)$/.exec(header);
174
+ if (!match || match[1] !== record.objectId) {throw missingSource(record.path);}
175
+ const size = Number(match[2]);
176
+ const start = headerEnd + 1;
177
+ if (!Number.isSafeInteger(size) || start + size >= output.length || output[start + size] !== 0x0a) {
178
+ throw missingSource(record.path);
179
+ }
180
+ offset = start + size + 1;
181
+ return { ...record, blob: Buffer.from(output.subarray(start, start + size)) };
182
+ });
183
+ if (offset !== output.length) {
184
+ throw new Error('kernel contract authority sources: git returned more than was asked for');
185
+ }
186
+ return sources;
187
+ }
188
+
189
+ /**
190
+ * The source-path set the kernel declares AT `sourceCommit`, read from that
191
+ * commit's own declaration blob: the same tree the digest is recomputed from,
192
+ * never the list of the contracts version this CLI bundles.
193
+ */
194
+ export function sourcePathSetAtCommit(repositoryDirectory, sourceCommit) {
195
+ const [declaration] = gitBlobsAtCommit(repositoryDirectory, sourceCommit, [
196
+ KERNEL_CONTRACT_SOURCE_PATH_SET_FILE,
197
+ ]);
198
+ return parseKernelContractSourcePathSet(declaration.blob.toString('utf8'), sourceCommit);
199
+ }
200
+
201
+ /**
202
+ * Refuses `sourcePaths` unless they equal the source-path set at `sourceCommit`
203
+ * (KernelContractSourcePathSetMismatchError names the missing and extra paths).
204
+ * The wire schema checks shape only; this is the check that the set means
205
+ * something.
206
+ */
207
+ export function assertSourcePathsMatchCommit(repositoryDirectory, sourceCommit, sourcePaths) {
208
+ assertSourcePathsEqualSet(sourcePaths, sourcePathSetAtCommit(repositoryDirectory, sourceCommit), sourceCommit);
209
+ }
210
+
211
+ /** The canonical JSON of an authority (sorted keys), or null for a value canonical evidence JSON cannot hold. */
212
+ function exactAuthorityJson(value) {
213
+ try {
214
+ return canonicalJson(value);
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /** `actual` is exactly `expected`, field for field and value for value; key order is not part of an authority. */
221
+ export function assertKernelContractAuthorityEqual(actual, expected, label) {
222
+ const canonical = exactAuthorityJson(actual);
223
+ if (canonical === null || canonical !== exactAuthorityJson(expected)) {
224
+ throw new Error(
225
+ `${label} does not exactly match kernel contract authority`,
226
+ );
227
+ }
228
+ return actual;
229
+ }
230
+
231
+ export async function readKernelContractAuthorityAtCommit({
232
+ repositoryDirectory = kernelRepoDir(),
233
+ sourceCommit,
234
+ schemas,
235
+ }) {
236
+ if (!schemas) {
237
+ throw new Error('trusted kernel distribution schemas are required');
238
+ }
239
+ sameRepositoryOrigin(repositoryDirectory, schemas);
240
+ assertCommitExists(repositoryDirectory, sourceCommit);
241
+ // The set is the commit's own (XEMA-2010): an authority at a commit whose
242
+ // contracts declare 446 paths is read, digested and stamped over those 446,
243
+ // whatever this CLI bundles. The digest's validateSources checks shape only
244
+ // (contracts 0.110.3 and later).
245
+ const sourcePaths = sourcePathSetAtCommit(repositoryDirectory, sourceCommit);
246
+ const sources = gitBlobsAtCommit(repositoryDirectory, sourceCommit, sourcePaths);
247
+ const contentDigest =
248
+ await schemas.computeKernelContractAuthorityContentDigest(sources);
249
+ return {
250
+ schemaVersion: 1,
251
+ generatorIdentity: schemas.XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR,
252
+ repositoryUrl: schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL,
253
+ sourceCommit,
254
+ sourcePaths,
255
+ contentHashAlgorithm:
256
+ schemas.KernelContractContentHashAlgorithm.LengthFramedGitPathModeBlobV1,
257
+ contentDigest,
258
+ };
259
+ }
260
+
261
+ export function assertExactCleanKernelContractCheckout({
262
+ repositoryDirectory = kernelRepoDir(),
263
+ sourceCommit,
264
+ schemas,
265
+ }) {
266
+ sameRepositoryOrigin(repositoryDirectory, schemas);
267
+ const head = exactHeadCommit(repositoryDirectory);
268
+ if (head !== sourceCommit) {
269
+ throw new Error(
270
+ `kernel contract repository HEAD ${head} does not match authority commit ${sourceCommit}`,
271
+ );
272
+ }
273
+ const status = git(
274
+ repositoryDirectory,
275
+ ['status', '--porcelain=v1', '-z', '--untracked-files=all'],
276
+ 'buffer',
277
+ );
278
+ if (status.byteLength !== 0) {
279
+ throw new Error(
280
+ 'kernel contract repository must be exactly clean for trusted generation',
281
+ );
282
+ }
283
+ const tracked = git(repositoryDirectory, ['ls-files', '-v', '-z'], 'buffer')
284
+ .toString('utf8')
285
+ .split('\0')
286
+ .filter(Boolean);
287
+ const hidden = tracked.find((record) => !record.startsWith('H '));
288
+ if (hidden) {
289
+ throw new Error(
290
+ `kernel contract repository has skip-worktree, assume-unchanged, or unexpected index state: ${hidden.slice(2)}`,
291
+ );
292
+ }
293
+ // ONE index listing and ONE batched blob read for the whole source set.
294
+ const indexByPath = new Map();
295
+ for (const record of git(repositoryDirectory, ['ls-files', '--stage', '-v', '-z'], 'buffer')
296
+ .toString('utf8')
297
+ .split('\0')
298
+ .filter(Boolean)) {
299
+ const path = record.slice(record.indexOf('\t') + 1);
300
+ indexByPath.set(path, [...(indexByPath.get(path) ?? []), record]);
301
+ }
302
+ const sourcePaths = sourcePathSetAtCommit(repositoryDirectory, sourceCommit);
303
+ const committed = new Map(
304
+ gitBlobsAtCommit(repositoryDirectory, sourceCommit, sourcePaths).map((source) => [source.path, source]),
305
+ );
306
+ for (const path of sourcePaths) {
307
+ const indexRecords = indexByPath.get(path) ?? [];
308
+ const indexMatch =
309
+ indexRecords.length === 1
310
+ ? /^H (100644) ([a-f0-9]{40,64}) 0\t(.+)$/.exec(indexRecords[0])
311
+ : null;
312
+ if (!indexMatch || indexMatch[3] !== path) {
313
+ throw new Error(
314
+ `kernel contract source has hidden flags or unexpected index state: ${path}`,
315
+ );
316
+ }
317
+ const source = committed.get(path);
318
+ const worktreePath = join(repositoryDirectory, path);
319
+ const stats = lstatSync(worktreePath);
320
+ if (
321
+ source.mode !== schemas.XEMA_KERNEL_CONTRACT_SOURCE_MODE ||
322
+ source.objectId !== indexMatch[2] ||
323
+ !stats.isFile() ||
324
+ stats.isSymbolicLink() ||
325
+ (stats.mode & 0o777) !== 0o644 ||
326
+ !readFileSync(worktreePath).equals(source.blob)
327
+ ) {
328
+ throw new Error(
329
+ `kernel contract source does not exactly match its committed Git mode/blob: ${path}`,
330
+ );
331
+ }
332
+ }
333
+ return head;
334
+ }
335
+
336
+ /** The flag by which a caller STATES the kernel commit a new contract authority is taken from. */
337
+ export const KERNEL_COMMIT_FLAG = '--kernel-commit';
338
+
339
+ /**
340
+ * The contract authority of the kernel checkout, held to the commit the caller
341
+ * STATES. A clean checkout at the kernel's origin URL proves only which
342
+ * repository the directory claims to be, never which commit somebody meant to
343
+ * pin: `kernelCommit` is that decision, it is required, and the checkout's HEAD
344
+ * must equal it. It is the `sourceCommit` the authority records.
345
+ */
346
+ export async function buildTrustedKernelContractAuthority({
347
+ repositoryDirectory = kernelRepoDir(),
348
+ schemas,
349
+ kernelCommit,
350
+ }) {
351
+ if (typeof kernelCommit !== 'string' || !COMMIT_PATTERN.test(kernelCommit)) {
352
+ throw new Error(
353
+ `${KERNEL_COMMIT_FLAG} <40-hex> is required: a new kernel contract authority is pinned to the commit the caller states, never to whatever the checkout happens to hold`,
354
+ );
355
+ }
356
+ const head = exactHeadCommit(repositoryDirectory);
357
+ if (head !== kernelCommit) {
358
+ throw new Error(
359
+ `kernel contract repository HEAD ${head} is not the stated ${KERNEL_COMMIT_FLAG} ${kernelCommit}: check out the stated commit, or state the commit you mean to pin`,
360
+ );
361
+ }
362
+ const sourceCommit = kernelCommit;
363
+ assertExactCleanKernelContractCheckout({
364
+ repositoryDirectory,
365
+ sourceCommit,
366
+ schemas,
367
+ });
368
+ // Trusted generation at HEAD ONLY: a NEW pin is minted by a CLI whose bundled
369
+ // contracts declare exactly the set at that commit, so the CLI that writes
370
+ // the pin understands every source it binds. Reading or validating an
371
+ // existing pin never makes this comparison.
372
+ assertSourcePathsEqualSet(
373
+ schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS,
374
+ sourcePathSetAtCommit(repositoryDirectory, sourceCommit),
375
+ sourceCommit,
376
+ "this CLI's bundled kernel contract source paths",
377
+ );
378
+ const authority = await readKernelContractAuthorityAtCommit({
379
+ repositoryDirectory,
380
+ sourceCommit,
381
+ schemas,
382
+ });
383
+ assertExactCleanKernelContractCheckout({
384
+ repositoryDirectory,
385
+ sourceCommit,
386
+ schemas,
387
+ });
388
+ return authority;
389
+ }
390
+
391
+ /**
392
+ * Is the pinned authority on the kernel history we ship — and has the kernel's
393
+ * contract surface moved since it?
394
+ *
395
+ * THIS USED TO REFUSE ON ANY CONTENT DRIFT, and that made the image-contract
396
+ * pin an input every kernel commit could invalidate. Measured 2026-10-01:
397
+ * `dist-check` was red with "the kernel contract surface CHANGED" because
398
+ * kernel `a348e71` added one member, `ComposedProcess`, to
399
+ * `RuntimeImageSubjectKind` — a kind no committed image lock carries and this
400
+ * repository never mints. The printed remedy, `xema-edition index runtime-image-source
401
+ * --repin-contract-authority`, rewrites `contractAuthority` in EVERY `images.lock.json`,
402
+ * which changes each lock's `normalizedImageLockDigest` (bound by the signed
403
+ * completeness statement) and orphans every signed producer statement (whose
404
+ * `predicate.contractAuthority` must equal the pin). Nine sealed editions, plus
405
+ * every customer edition minted under the same pin, would have lost their
406
+ * evidence for a change that altered no image. The same would follow from a
407
+ * new export in `distribution/index.ts`, which is one of the authority's
408
+ * source paths and is touched by every distribution-contract addition.
409
+ *
410
+ * Owner ruling, 2026-10-01: an unrelated change must not force historical
411
+ * editions to re-resolve or re-seal their image lists unless IMAGE SEMANTICS
412
+ * changed. The pin is a DELIBERATE image-contract version, advanced by the
413
+ * repin transaction when an edition needs semantics the pinned contract cannot
414
+ * describe — never by kernel HEAD moving underneath it.
415
+ *
416
+ * So this now REFUSES only what is a defect:
417
+ * - a pin that is not an ancestor of kernel HEAD (a different history — see
418
+ * below), and
419
+ * - (in the caller) a pin whose recorded digest does not re-derive from its
420
+ * own commit's blobs.
421
+ * Drift is RETURNED as the list of moved source paths, and the caller reports
422
+ * it. It is not silent: it is printed on every check, naming each path.
423
+ *
424
+ * What keeps a stale pin from LYING about an image lock: the standalone
425
+ * verifier is built from kernel source that descends from the pin and strict-
426
+ * parses every image lock and producer statement, and producer evidence is
427
+ * refused unless its signed `contractAuthority` equals the pin. A lock that
428
+ * used semantics newer than the pin therefore fails closed at verification,
429
+ * loudly — and that is the moment a repin is genuinely owed.
430
+ *
431
+ * The pin must still be an ancestor of HEAD. A pin on a different history is
432
+ * not "behind", it is unrelated, and recomputing a digest across a fork would
433
+ * compare content from a branch nobody is shipping.
434
+ */
435
+ async function assessAuthorityLineage({ authority, repositoryDirectory, schemas }) {
436
+ const head = exactHeadCommit(repositoryDirectory);
437
+ if (head === authority.sourceCommit) {return { head, changedPaths: [] };}
438
+
439
+ if (!isAncestor(repositoryDirectory, authority.sourceCommit, head)) {
440
+ throw new Error(
441
+ `kernel contract authority commit ${authority.sourceCommit} is not an ancestor of kernel HEAD ` +
442
+ `${head}. The pin is on a different history (a force push, or a checkout of another branch), ` +
443
+ 'so it cannot be compared by content — repin deliberately against the history you intend to ship.',
444
+ );
445
+ }
446
+
447
+ const atHead = await readKernelContractAuthorityAtCommit({
448
+ repositoryDirectory,
449
+ sourceCommit: head,
450
+ schemas,
451
+ });
452
+ if (atHead.contentDigest === authority.contentDigest) {
453
+ return { head, changedPaths: [] };
454
+ }
455
+
456
+ // The two commits may declare different sets: a path added or dropped since
457
+ // the pin has moved as surely as one whose blob changed.
458
+ const pinnedTree = gitTreeAtCommit(repositoryDirectory, authority.sourceCommit);
459
+ const currentTree = gitTreeAtCommit(repositoryDirectory, head);
460
+ const pinnedSet = new Set(authority.sourcePaths);
461
+ const currentSet = new Set(atHead.sourcePaths);
462
+ const changedPaths = [...new Set([...pinnedSet, ...currentSet])]
463
+ .sort()
464
+ .filter(
465
+ (path) =>
466
+ pinnedSet.has(path) !== currentSet.has(path) ||
467
+ pinnedTree.get(path)?.objectId !== currentTree.get(path)?.objectId,
468
+ );
469
+ return { head, changedPaths };
470
+ }
471
+
472
+ /** One line naming the drift, for callers that report it. */
473
+ export function describeKernelContractAuthorityDrift({ authority, head, changedPaths }) {
474
+ return (
475
+ `kernel contract authority ${authority.sourceCommit} is behind kernel HEAD ${head}: ` +
476
+ `${changedPaths.length} contract source path(s) moved (${changedPaths.join(', ')}). ` +
477
+ 'This is NOT a refusal — image locks stay pinned to the contract they were minted ' +
478
+ 'under. Repin (`xema-edition index runtime-image-source --repin-contract-authority`) only ' +
479
+ 'when an edition needs image semantics the pinned contract cannot describe; that ' +
480
+ 'repin re-binds every edition\'s image evidence.'
481
+ );
482
+ }
483
+
484
+ export async function validateKernelContractAuthorityFromGit({
485
+ authority,
486
+ repositoryDirectory = kernelRepoDir(),
487
+ schemas,
488
+ requireHeadMatch = true,
489
+ requireCleanCheckout = false,
490
+ // Called with `{ authority, head, changedPaths }` when the pin is an ancestor
491
+ // of HEAD but the contract surface has moved since it. Reporting, never
492
+ // refusing — see `assessAuthorityLineage`.
493
+ onDrift = () => {},
494
+ }) {
495
+ if (!authority || typeof authority !== 'object') {
496
+ throw new Error('kernel contract authority is required');
497
+ }
498
+ if (requireHeadMatch) {
499
+ const { head, changedPaths } = await assessAuthorityLineage({
500
+ authority,
501
+ repositoryDirectory,
502
+ schemas,
503
+ });
504
+ if (changedPaths.length > 0) {onDrift({ authority, head, changedPaths });}
505
+ }
506
+ if (requireCleanCheckout) {
507
+ assertExactCleanKernelContractCheckout({
508
+ repositoryDirectory,
509
+ sourceCommit: authority.sourceCommit,
510
+ schemas,
511
+ });
512
+ }
513
+ assertCommitExists(repositoryDirectory, authority.sourceCommit);
514
+ assertSourcePathsMatchCommit(repositoryDirectory, authority.sourceCommit, authority.sourcePaths);
515
+ const expected = await readKernelContractAuthorityAtCommit({
516
+ repositoryDirectory,
517
+ sourceCommit: authority.sourceCommit,
518
+ schemas,
519
+ });
520
+ assertKernelContractAuthorityEqual(
521
+ authority,
522
+ expected,
523
+ 'runtime image source index',
524
+ );
525
+ return authority;
526
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The kernel contract authority's source-path set, as the kernel commit itself
3
+ * declares it. The set belongs to the commit an authority is pinned at, never
4
+ * to the contracts package the CLI happens to bundle: a bundled constant is the
5
+ * set of ONE contracts version, and comparing a pin made under another version
6
+ * against it is the cross-version defect this check replaces.
7
+ */
8
+
9
+ import { KERNEL_PACKAGE_REPO_PATH } from './edition-workspace.mjs';
10
+
11
+ /** The file in the kernel repository that declares the source-path set. */
12
+ export const KERNEL_CONTRACT_SOURCE_PATH_SET_FILE =
13
+ `${KERNEL_PACKAGE_REPO_PATH}/src/distribution/lib/kernel-contract-source-paths.ts`;
14
+
15
+ const DECLARATION_START = 'export const XEMA_KERNEL_CONTRACT_SOURCE_PATHS = Object.freeze([';
16
+ const DECLARATION_END = '] as const);';
17
+ const PATH_LINE_PATTERN = /^ {2}'([^'\\\s]+)',$/;
18
+
19
+ /**
20
+ * A source-path list is not the source-path set at `sourceCommit`. `subject`
21
+ * names the list that was compared: a pinned authority's `sourcePaths`, or the
22
+ * set the CLI's bundled contracts declare (trusted generation only).
23
+ */
24
+ export class KernelContractSourcePathSetMismatchError extends Error {
25
+ constructor({ sourceCommit, missing, extra, subject = 'kernel contract authority sourcePaths' }) {
26
+ super(
27
+ `${subject} are not the source-path set at ${sourceCommit}: ` +
28
+ `${missing.length} missing (${missing.join(', ') || 'none'}), ` +
29
+ `${extra.length} extra (${extra.join(', ') || 'none'})`,
30
+ );
31
+ this.name = 'KernelContractSourcePathSetMismatchError';
32
+ this.sourceCommit = sourceCommit;
33
+ this.missing = missing;
34
+ this.extra = extra;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * The ordered path list declared by the text of
40
+ * {@link KERNEL_CONTRACT_SOURCE_PATH_SET_FILE}. Refuses any other shape, a list
41
+ * that is empty, not strictly ascending (which also makes it unique), or that
42
+ * does not name the declaration file itself: the declaration is one of the
43
+ * digested sources, so the digest commits to the set it was taken over.
44
+ */
45
+ export function parseKernelContractSourcePathSet(text, sourceCommit) {
46
+ const lines = text.split('\n');
47
+ const start = lines.indexOf(DECLARATION_START);
48
+ const end = lines.indexOf(DECLARATION_END, start + 1);
49
+ if (start === -1 || end === -1) {
50
+ throw new Error(
51
+ `${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} does not declare XEMA_KERNEL_CONTRACT_SOURCE_PATHS in the expected form`,
52
+ );
53
+ }
54
+ const paths = lines.slice(start + 1, end).map((line) => {
55
+ const match = PATH_LINE_PATTERN.exec(line);
56
+ if (!match || match[1].split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
57
+ throw new Error(
58
+ `${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} has a line that is not one quoted relative path: ${JSON.stringify(line)}`,
59
+ );
60
+ }
61
+ return match[1];
62
+ });
63
+ if (paths.length === 0) {
64
+ throw new Error(`${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} declares no source paths`);
65
+ }
66
+ for (let index = 1; index < paths.length; index += 1) {
67
+ if (!(paths[index - 1] < paths[index])) {
68
+ throw new Error(
69
+ `${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} is not in strictly ascending order at ${JSON.stringify(paths[index])}`,
70
+ );
71
+ }
72
+ }
73
+ if (!paths.includes(KERNEL_CONTRACT_SOURCE_PATH_SET_FILE)) {
74
+ throw new Error(
75
+ `${KERNEL_CONTRACT_SOURCE_PATH_SET_FILE} at ${sourceCommit} does not declare itself, so the digest would not commit to the set`,
76
+ );
77
+ }
78
+ return paths;
79
+ }
80
+
81
+ /**
82
+ * Refuses `sourcePaths` unless it equals `expected`, the set at `sourceCommit`
83
+ * (same paths, same order); names the paths `sourcePaths` lacks (missing) and
84
+ * the ones it has beyond the set (extra).
85
+ */
86
+ export function assertSourcePathsEqualSet(sourcePaths, expected, sourceCommit, subject = undefined) {
87
+ const have = new Set(sourcePaths);
88
+ const want = new Set(expected);
89
+ const missing = expected.filter((path) => !have.has(path));
90
+ const extra = sourcePaths.filter((path) => !want.has(path));
91
+ const sameOrder =
92
+ sourcePaths.length === expected.length && sourcePaths.every((path, index) => path === expected[index]);
93
+ if (missing.length > 0 || extra.length > 0 || !sameOrder) {
94
+ throw new KernelContractSourcePathSetMismatchError({ sourceCommit, missing, extra, subject });
95
+ }
96
+ }