@xemahq/xema 0.29.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +60 -389
  2. package/dist/commands/edition.js +3 -3
  3. package/dist/commands/edition.js.map +1 -1
  4. package/dist/edition/verify-image-evidence.bundle.mjs +8715 -807
  5. package/dist/edition/verify-image-evidence.bundle.provenance.json +873 -405
  6. package/edition/README.md +53 -273
  7. package/edition/adapter-bundled-versions.mjs +107 -0
  8. package/edition/adapter-host-versions.mjs +41 -0
  9. package/edition/authorization-base.mjs +18 -9
  10. package/edition/build-runtime-image-source-index.mjs +77 -4
  11. package/edition/catalogue.mjs +15 -12
  12. package/edition/customer-definition.mjs +3 -20
  13. package/edition/develop-customer-edition.mjs +2 -2
  14. package/edition/develop-edition-resolve.mjs +10 -2
  15. package/edition/develop-edition.mjs +56 -62
  16. package/edition/develop-freeze.mjs +14 -3
  17. package/edition/dist-check.mjs +1 -0
  18. package/edition/distribution-evidence-set.mjs +32 -5
  19. package/edition/edition-channel.mjs +15 -1
  20. package/edition/edition-locks.mjs +16 -6
  21. package/edition/edition-protocol.mjs +30 -8
  22. package/edition/edition-workspace.mjs +8 -0
  23. package/edition/image-evidence.mjs +133 -25
  24. package/edition/kernel-package.mjs +193 -0
  25. package/edition/lib.mjs +73 -1
  26. package/edition/oci-evidence-verbs.mjs +2 -2
  27. package/edition/plan-image-evidence-refresh.mjs +2 -2
  28. package/edition/produce-distribution-completeness-evidence.mjs +5 -0
  29. package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
  30. package/edition/release-catalogue.mjs +10 -3
  31. package/edition/resolve-chart-set.mjs +1 -1
  32. package/edition/resolve-customer-edition.mjs +29 -33
  33. package/edition/resolve-distribution.mjs +5 -1
  34. package/edition/resolve-edition.mjs +3 -2
  35. package/edition/seal-image-digests.mjs +16 -4
  36. package/edition/trust-policy-arming-verb.mjs +2 -10
  37. package/edition/verify-image-evidence.mjs +1 -0
  38. package/examples/README.md +3 -2
  39. package/package.json +3 -3
@@ -0,0 +1,193 @@
1
+ // kernel-package — the customer edition's kernel: `@xemahq/contracts` taken from public npm.
2
+ //
3
+ // A catalogue names its kernel package (`kernel: {name, version, integrity}`, catalogue.mjs). This module is
4
+ // the ONE place that touches npm: it downloads that exact tarball (public, no credential, no .npmrc, no
5
+ // npm/pnpm process), holds it by the catalogue's integrity BEFORE anything is extracted, extracts it with the
6
+ // system `tar`, links its runtime dependencies to the copies this CLI itself ships, and proves the package is
7
+ // the contract authority the catalogue was resolved under: first by its edition protocol identity (data, no
8
+ // code executed), and only then, by loading its schemas, by the constants the authority was derived with.
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import { createHash } from 'node:crypto';
12
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { tmpdir } from 'node:os';
15
+ import { dirname, join } from 'node:path';
16
+ import { isDeepStrictEqual } from 'node:util';
17
+
18
+ import { readKernelEditionProtocol } from './edition-protocol.mjs';
19
+ import { cacheDir, stateKernelPackageDir } from './edition-workspace.mjs';
20
+ import { loadKernelDistributionSchemas } from './lib.mjs';
21
+
22
+ export const KERNEL_PACKAGE_NAME = '@xemahq/contracts';
23
+ const MAX_TARBALL_BYTES = 64 * 1024 * 1024;
24
+ const DEPENDENCY_NAME = /^(?:@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/;
25
+ const EXACT = /^(\d+)\.(\d+)\.(\d+)$/;
26
+ const CARET = /^\^(\d+)\.(\d+)\.(\d+)$/;
27
+
28
+ export class KernelPackageError extends Error {
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = 'KernelPackageError';
32
+ }
33
+ }
34
+
35
+ export const kernelTarballUrl = (version) => `https://registry.npmjs.org/${KERNEL_PACKAGE_NAME}/-/contracts-${version}.tgz`;
36
+
37
+ /** The npm integrity string of `bytes`: `sha512-<base64>`. */
38
+ export const integrityOf = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
39
+
40
+ /** The published tarball of `@xemahq/contracts@version`, bounded in size. */
41
+ export async function fetchKernelTarball(version, { fetch = globalThis.fetch } = {}) {
42
+ const url = kernelTarballUrl(version);
43
+ const response = await fetch(url);
44
+ if (response.status === 404) {
45
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME}@${version} is not published on npm (${url}): a catalogue names a published kernel`);
46
+ }
47
+ if (!response.ok) {throw new KernelPackageError(`${url} answered HTTP ${response.status}`);}
48
+ const declared = Number(response.headers.get('content-length') ?? 0);
49
+ if (declared > MAX_TARBALL_BYTES) {throw new KernelPackageError(`${url} declares ${declared} bytes, over the ${MAX_TARBALL_BYTES} byte bound`);}
50
+ const chunks = [];
51
+ let size = 0;
52
+ for await (const chunk of response.body) {
53
+ size += chunk.length;
54
+ if (size > MAX_TARBALL_BYTES) {throw new KernelPackageError(`${url} is over the ${MAX_TARBALL_BYTES} byte bound`);}
55
+ chunks.push(chunk);
56
+ }
57
+ return Buffer.concat(chunks);
58
+ }
59
+
60
+ /**
61
+ * Does `version` (exactly x.y.z) satisfy `range`? Exactly two range forms are supported: `x.y.z`, and
62
+ * `^x.y.z` (x > 0: >=x.y.z <(x+1).0.0; 0.y.z: >=0.y.z <0.(y+1).0). Any other form is refused.
63
+ */
64
+ export function satisfiesDeclaredRange(version, range) {
65
+ const have = EXACT.exec(version)?.slice(1).map(Number);
66
+ if (have === undefined) {throw new KernelPackageError(`version ${JSON.stringify(version)} is not an exact x.y.z version`);}
67
+ const exact = EXACT.exec(range)?.slice(1).map(Number);
68
+ if (exact !== undefined) {return isDeepStrictEqual(have, exact);}
69
+ const caret = CARET.exec(range)?.slice(1).map(Number);
70
+ if (caret === undefined) {throw new KernelPackageError(`range ${JSON.stringify(range)} is not x.y.z or ^x.y.z, the only forms a kernel dependency may declare`);}
71
+ const compare = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
72
+ const upper = caret[0] > 0 ? [caret[0] + 1, 0, 0] : [0, caret[1] + 1, 0];
73
+ return compare(have, caret) >= 0 && compare(have, upper) < 0;
74
+ }
75
+
76
+ /** The directory of `name` as this CLI itself ships it: its own resolution first, then its installed kernel's. */
77
+ function shippedPackageDir(name) {
78
+ const own = createRequire(import.meta.url);
79
+ const attempts = [() => own, () => createRequire(realpathSync(own.resolve(`${KERNEL_PACKAGE_NAME}/package.json`)))];
80
+ for (const requireFrom of attempts) {
81
+ try {
82
+ return dirname(realpathSync(requireFrom().resolve(`${name}/package.json`)));
83
+ } catch (error) {
84
+ if (error?.code !== 'MODULE_NOT_FOUND' && error?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {throw error;}
85
+ }
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function linkRuntimeDependencies(packageDir) {
91
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
92
+ const nodeModules = dirname(manifest.name.startsWith('@') ? dirname(packageDir) : packageDir);
93
+ for (const [name, range] of Object.entries(manifest.dependencies ?? {})) {
94
+ if (!DEPENDENCY_NAME.test(name)) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} declares a dependency named ${JSON.stringify(name)}, which is not a package name`);}
95
+ const shipped = shippedPackageDir(name);
96
+ if (shipped === undefined) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} depends on ${name}@${range}, which this CLI does not ship`);}
97
+ const version = JSON.parse(readFileSync(join(shipped, 'package.json'), 'utf8')).version;
98
+ let ok;
99
+ try {
100
+ ok = satisfiesDeclaredRange(version, range);
101
+ } catch (error) {
102
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} dependency ${name}: ${error.message} (this CLI ships ${version})`);
103
+ }
104
+ if (!ok) {throw new KernelPackageError(`${KERNEL_PACKAGE_NAME} depends on ${name}@${range}, but this CLI ships ${name}@${version}`);}
105
+ const link = join(nodeModules, name);
106
+ mkdirSync(dirname(link), { recursive: true });
107
+ symlinkSync(shipped, link, 'dir');
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Download the catalogue's kernel, hold it to `kernel.integrity` (refused before anything is written), extract it
113
+ * under `<cacheDir>/kernel-npm/<version>-<16 hex of the sha512>/node_modules/@xemahq/contracts` (fresh every run)
114
+ * and link its runtime dependencies. Returns the package directory.
115
+ */
116
+ export async function materializeKernelPackage({ kernel, cacheDir, fetchTarball = fetchKernelTarball }) {
117
+ const bytes = await fetchTarball(kernel.version);
118
+ const actual = integrityOf(bytes);
119
+ if (actual !== kernel.integrity) {
120
+ throw new KernelPackageError(`${KERNEL_PACKAGE_NAME}@${kernel.version} integrity ${actual} is not the catalogue's kernel.integrity ${kernel.integrity}`);
121
+ }
122
+ const hex = createHash('sha512').update(bytes).digest('hex').slice(0, 16);
123
+ const root = join(cacheDir, 'kernel-npm', `${kernel.version}-${hex}`);
124
+ const packageDir = join(root, 'node_modules', ...KERNEL_PACKAGE_NAME.split('/'));
125
+ rmSync(root, { recursive: true, force: true });
126
+ mkdirSync(packageDir, { recursive: true });
127
+ const extracted = spawnSync('tar', ['-xzf', '-', '-C', packageDir, '--strip-components', '1'], { input: bytes, stdio: ['pipe', 'ignore', 'pipe'] });
128
+ if (extracted.error !== undefined) {throw new KernelPackageError(`tar could not run: ${extracted.error.message}`);}
129
+ if (extracted.status !== 0) {throw new KernelPackageError(`tar failed extracting ${KERNEL_PACKAGE_NAME}@${kernel.version}: ${extracted.stderr.toString().trim()}`);}
130
+ const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
131
+ if (manifest.name !== KERNEL_PACKAGE_NAME || manifest.version !== kernel.version) {
132
+ throw new KernelPackageError(`the tarball holds ${manifest.name}@${manifest.version}, not ${KERNEL_PACKAGE_NAME}@${kernel.version}`);
133
+ }
134
+ linkRuntimeDependencies(packageDir);
135
+ return packageDir;
136
+ }
137
+
138
+ const refuse = (field, wanted, got) => new KernelPackageError(`the kernel package is not the catalogue's contract authority: ${field} is ${JSON.stringify(got)}, the catalogue states ${JSON.stringify(wanted)}`);
139
+
140
+ /**
141
+ * The kernel package at `kernelPackageDir` is the contract authority of `catalogue`. First its identity, read as
142
+ * data (final, which the protocol read refuses otherwise, and the catalogue's digest); only then are its schemas loaded and held to the authority's constants.
143
+ * `sourceCommit` is provenance and is not compared. Returns the loaded schemas.
144
+ */
145
+ export async function assertKernelPackageIsCatalogueAuthority({ catalogue, kernelPackageDir, loadSchemas }) {
146
+ const authority = catalogue.contractAuthority;
147
+ const identity = readKernelEditionProtocol(kernelPackageDir);
148
+ if (identity.contentDigest !== authority.contentDigest) {throw refuse('contentDigest', authority.contentDigest, identity.contentDigest);}
149
+ const schemas = await loadSchemas();
150
+ if (!isDeepStrictEqual(authority.sourcePaths, schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS)) {
151
+ throw refuse('sourcePaths', authority.sourcePaths, schemas.XEMA_KERNEL_CONTRACT_SOURCE_PATHS);
152
+ }
153
+ const constants = [
154
+ ['generatorIdentity', schemas.XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR],
155
+ ['repositoryUrl', schemas.XEMA_KERNEL_CONTRACT_REPOSITORY_URL],
156
+ ['contentHashAlgorithm', schemas.KernelContractContentHashAlgorithm.LengthFramedGitPathModeBlobV1],
157
+ ];
158
+ for (const [field, kernelValue] of constants) {
159
+ if (authority[field] !== kernelValue) {throw refuse(field, authority[field], kernelValue);}
160
+ }
161
+ return schemas;
162
+ }
163
+
164
+ /**
165
+ * The `kernel` row a VENDOR catalogue carries: the published `@xemahq/contracts` the vendor's own kernel
166
+ * (`kernelPackageDir`) is, taken from npm, proved to be `contractAuthority` and held by its integrity.
167
+ * An unpublished version is refused. The downloaded package is extracted to a temporary directory and removed.
168
+ */
169
+ export async function kernelPackageOfAuthority({ kernelPackageDir, contractAuthority, fetchTarball = fetchKernelTarball, loadSchemas = loadKernelDistributionSchemas }) {
170
+ const manifest = JSON.parse(readFileSync(join(kernelPackageDir, 'package.json'), 'utf8'));
171
+ if (manifest.name !== KERNEL_PACKAGE_NAME) {throw new KernelPackageError(`the vendor kernel is ${manifest.name}, not ${KERNEL_PACKAGE_NAME}`);}
172
+ const bytes = await fetchTarball(manifest.version);
173
+ const kernel = { name: KERNEL_PACKAGE_NAME, version: manifest.version, integrity: integrityOf(bytes) };
174
+ const scratch = mkdtempSync(join(tmpdir(), 'xema-kernel-npm-'));
175
+ try {
176
+ const packageDir = await materializeKernelPackage({ kernel, cacheDir: scratch, fetchTarball: async () => bytes });
177
+ await assertKernelPackageIsCatalogueAuthority({ catalogue: { contractAuthority }, kernelPackageDir: packageDir, loadSchemas });
178
+ } finally {
179
+ rmSync(scratch, { recursive: true, force: true });
180
+ }
181
+ return kernel;
182
+ }
183
+
184
+ /**
185
+ * A customer verb's one step between fetching its catalogue and using any kernel schema: the catalogue's kernel is
186
+ * downloaded into the cache root, stated as the workspace's Kernel root, and proved to be the catalogue's contract
187
+ * authority. Returns the kernel's distribution schemas.
188
+ */
189
+ export async function adoptCatalogueKernel(catalogue) {
190
+ const kernelPackageDir = await materializeKernelPackage({ kernel: catalogue.kernel, cacheDir: cacheDir() });
191
+ stateKernelPackageDir(kernelPackageDir);
192
+ return assertKernelPackageIsCatalogueAuthority({ catalogue, kernelPackageDir, loadSchemas: loadKernelDistributionSchemas });
193
+ }
package/edition/lib.mjs CHANGED
@@ -14,6 +14,9 @@ import { createRequire } from 'node:module';
14
14
  import { isIP } from 'node:net';
15
15
  import { join } from 'node:path';
16
16
 
17
+ import { parse as parseYaml } from 'yaml';
18
+
19
+ import { ADAPTER_HOST_BIOME_ID, readHostPackageVersions } from './adapter-host-versions.mjs';
17
20
  import { readBiomeAuthorizationModuleAt } from './authorization-base.mjs';
18
21
  import {
19
22
  isAuthoritativeProductionBiomeSource,
@@ -688,6 +691,18 @@ export async function loadKernelDistributionSchemas() {
688
691
  `Underlying error: ${err.message}`,
689
692
  );
690
693
  }
694
+ // The Node-only half of the distribution contracts (`distribution/server`): the lock digest and the
695
+ // predicate checks that recompute it.
696
+ let server;
697
+ try {
698
+ server = await import(`file://${kernelDistPath('distribution', 'server', 'index.js')}`);
699
+ } catch (err) {
700
+ throw new Error(
701
+ `Cannot load the built kernel distribution/server module from ${kernelDistPath('distribution', 'server', 'index.js')}. ` +
702
+ `Rebuild the kernel checkout (${kernelRepoDir()}). Underlying error: ${err.message}`,
703
+ );
704
+ }
705
+ const { distributionLockDigest, distributionImagesPredicateLockRefusal, imageDeliveryEntitlementRefusal } = server;
691
706
  const {
692
707
  parseDistribution,
693
708
  parseDistributionLock,
@@ -695,6 +710,8 @@ export async function loadKernelDistributionSchemas() {
695
710
  parseUnsealedDistributionLock,
696
711
  parseCustomerEditionDefinition,
697
712
  customerTrustCeilingIssues,
713
+ customerLockCeilingIssues,
714
+ biomeOriginsOfLock,
698
715
  sealDistributionLock,
699
716
  unsealedProjection,
700
717
  UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
@@ -714,6 +731,7 @@ export async function loadKernelDistributionSchemas() {
714
731
  SigstoreEvidenceDescriptorSchema,
715
732
  parsePlatformServiceSourceIndex,
716
733
  parseRuntimeImageSourceIndex,
734
+ producerWorkflowPinRefusal,
717
735
  resolveRuntimeImagePlatforms,
718
736
  BiomeInstallPolicy,
719
737
  BiomeLaunchMode,
@@ -749,6 +767,7 @@ export async function loadKernelDistributionSchemas() {
749
767
  describeProvenance,
750
768
  describeHop,
751
769
  deriveWorkflowSubjects,
770
+ distributionImagesPredicateChannelRefusal,
752
771
  } = mod;
753
772
  let topology;
754
773
  try {
@@ -791,6 +810,7 @@ export async function loadKernelDistributionSchemas() {
791
810
  typeof SigstoreEvidenceDescriptorSchema?.parse !== 'function' ||
792
811
  typeof parsePlatformServiceSourceIndex !== 'function' ||
793
812
  typeof parseRuntimeImageSourceIndex !== 'function' ||
813
+ typeof producerWorkflowPinRefusal !== 'function' ||
794
814
  typeof resolveRuntimeImagePlatforms !== 'function' ||
795
815
  typeof resolveFirstPartyCapabilityDomains !== 'function' ||
796
816
  typeof DistributionVisibility !== 'object' ||
@@ -837,6 +857,12 @@ export async function loadKernelDistributionSchemas() {
837
857
  // A biome's workflow subjects and its edition trust ceiling, each written
838
858
  // through the kernel's one derivation (`@xemahq/contracts` >= 0.71.0).
839
859
  typeof deriveWorkflowSubjects !== 'function' ||
860
+ // Predicate v2 checks (`@xemahq/contracts` >= 0.78.0): the channel the verifying policy row states and
861
+ // the lock digest the predicates bind.
862
+ typeof distributionImagesPredicateChannelRefusal !== 'function' ||
863
+ typeof distributionImagesPredicateLockRefusal !== 'function' ||
864
+ typeof distributionLockDigest !== 'function' ||
865
+ typeof imageDeliveryEntitlementRefusal !== 'function' ||
840
866
  typeof deriveTrustCeiling !== 'function' ||
841
867
  // The trust-domain vocabulary a customer edition is bounded by: the vendor's
842
868
  // own domain, a biome's default domain, and the two ceiling grammars a
@@ -867,6 +893,8 @@ export async function loadKernelDistributionSchemas() {
867
893
  parseUnsealedDistributionLock,
868
894
  parseCustomerEditionDefinition,
869
895
  customerTrustCeilingIssues,
896
+ customerLockCeilingIssues,
897
+ biomeOriginsOfLock,
870
898
  sealDistributionLock,
871
899
  unsealedProjection,
872
900
  UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
@@ -926,6 +954,11 @@ export async function loadKernelDistributionSchemas() {
926
954
  FIRST_PARTY_TRUST_DOMAIN,
927
955
  ProcessTrustPolicySchema,
928
956
  DistributionTrustPolicySchema,
957
+ distributionLockDigest,
958
+ distributionImagesPredicateChannelRefusal,
959
+ distributionImagesPredicateLockRefusal,
960
+ imageDeliveryEntitlementRefusal,
961
+ producerWorkflowPinRefusal,
929
962
  };
930
963
  }
931
964
 
@@ -1086,6 +1119,10 @@ function listBiomeManifestsAt(directory, repo, commit) {
1086
1119
  rel,
1087
1120
  repo,
1088
1121
  path,
1122
+ // The manifest's directory in its repository, and a reader of any file of
1123
+ // that repository at the SAME pinned commit (the host's pnpm-lock.yaml).
1124
+ manifestDirectory,
1125
+ readRepositoryText: (repoRelativePath) => readGitTextAtCommitOrNull(directory, repoRelativePath, commit),
1089
1126
  ...(authorizationModule === null ? {} : { authorizationModule }),
1090
1127
  });
1091
1128
  }
@@ -2058,6 +2095,16 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2058
2095
  .filter((name) => typeof name === 'string' && name.length > 0),
2059
2096
  ),
2060
2097
  ].sort(compareStrings),
2098
+ // The capabilities the signed biome declares (a delegation
2099
+ // origination may lend only these): the manifest's
2100
+ // `xema.exposesCapabilities`; `requires*` are what it asks for.
2101
+ declaredCapabilities: [
2102
+ ...new Set(
2103
+ (Array.isArray(xema.exposesCapabilities) ? xema.exposesCapabilities : []).filter(
2104
+ (ref) => typeof ref === 'string' && ref.length > 0,
2105
+ ),
2106
+ ),
2107
+ ].sort(compareStrings),
2061
2108
  },
2062
2109
  },
2063
2110
  }
@@ -2263,6 +2310,26 @@ export function validateServiceDependencyOwnerParity(
2263
2310
  return entries;
2264
2311
  }
2265
2312
 
2313
+ /**
2314
+ * The adapter host's resolved `@xemahq/contracts` / `@xemahq/biome-sdk` versions, read from its source
2315
+ * repository's pnpm-lock.yaml at the pinned commit. An INDEX field only: the distribution lock never carries it.
2316
+ */
2317
+ function adapterHostPackageVersions(manifest) {
2318
+ const services = (manifest.raw.xema.components ?? []).filter(
2319
+ (component) => component?.kind === 'service' && component.artifact?.kind === 'oci-image',
2320
+ );
2321
+ if (services.length !== 1) {
2322
+ throw new Error(`${manifest.rel}: the adapter host declares ${services.length} oci-image service components; exactly one carries its package`);
2323
+ }
2324
+ const lockfilePath = 'pnpm-lock.yaml';
2325
+ return readHostPackageVersions({
2326
+ lockfileText: manifest.readRepositoryText(lockfilePath),
2327
+ importer: `${manifest.manifestDirectory}/${services[0].artifact.path}`,
2328
+ lockfilePath: `${manifest.repo}/${lockfilePath}`,
2329
+ parseYaml,
2330
+ });
2331
+ }
2332
+
2266
2333
  export async function buildBiomeIndex(
2267
2334
  platformServiceIndex,
2268
2335
  sourceRepositories = distributionBiomeSourceRepositories(),
@@ -2275,7 +2342,12 @@ export async function buildBiomeIndex(
2275
2342
  });
2276
2343
  const entries = [];
2277
2344
  for (const manifest of await committedBiomeManifests(sourceRepositories, authority)) {
2278
- entries.push(await manifestToIndexEntry(manifest));
2345
+ const entry = await manifestToIndexEntry(manifest);
2346
+ entries.push(
2347
+ entry.id === ADAPTER_HOST_BIOME_ID
2348
+ ? { ...entry, hostPackageVersions: adapterHostPackageVersions(manifest) }
2349
+ : entry,
2350
+ );
2279
2351
  }
2280
2352
  resolveServiceDependencyOwners(entries, platformServiceIndex);
2281
2353
  assertDeclaredDatabasesUnowned(entries);
@@ -32,7 +32,7 @@ import {
32
32
  isEntryModule,
33
33
  } from './edition-workspace.mjs';
34
34
  import { SHA256_DIGEST_PATTERN } from './evidence-grammars.mjs';
35
- import { canonicalJson, loadValidatedLocks } from './image-evidence.mjs';
35
+ import { canonicalJson, loadValidatedImageLocks } from './image-evidence.mjs';
36
36
  import { compareStrings, loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
37
37
  import {
38
38
  IMAGE_SOURCE_EVIDENCE_ARTIFACT_TYPE,
@@ -230,7 +230,7 @@ export async function planOciEvidenceBackfill({
230
230
  const missingGit = new Map();
231
231
  for (const distributionId of distributionIds) {
232
232
  assertCanonicalDistributionId(distributionId);
233
- const { imageLock, repositoryPins } = await loadValidatedLocks(distributionId, data, schemas, {
233
+ const { imageLock, repositoryPins } = await loadValidatedImageLocks(distributionId, data, schemas, {
234
234
  editionsRoot: editions,
235
235
  });
236
236
  for (const subject of imageLock.subjects) {
@@ -66,7 +66,7 @@ import {
66
66
  } from './edition-workspace.mjs';
67
67
  import {
68
68
  canonicalJson,
69
- loadValidatedLocks,
69
+ loadValidatedImageLocks,
70
70
  } from './image-evidence.mjs';
71
71
  import { assertKernelContractAuthorityEqual } from './kernel-contract-authority.mjs';
72
72
  import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
@@ -142,7 +142,7 @@ export async function planImageEvidenceRefresh({
142
142
 
143
143
  const subjects = [];
144
144
  for (const distributionId of [...distributionIds].sort(compareStrings)) {
145
- const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedLocks(
145
+ const { imageLock, repositoryPins, runtimeSourceIndex } = await loadValidatedImageLocks(
146
146
  distributionId,
147
147
  dataRootDirectory,
148
148
  schemas,
@@ -21,6 +21,7 @@ import {
21
21
  assertPinnedCosignVersion,
22
22
  buildDistributionImageEvidenceSet,
23
23
  buildDistributionImagesStatement,
24
+ evidenceChannelOfSigner,
24
25
  loadPinnedTrustPolicy,
25
26
  parseEvidenceCliFlags,
26
27
  resolveProducerEvidence,
@@ -59,6 +60,8 @@ export function githubWorkflowSigner({ repository, ref, path }) {
59
60
 
60
61
  /** The producer evidence and the completeness statement these inputs resolve to; deterministic. */
61
62
  async function deriveCompleteness(options, { outputRoot }) {
63
+ // The channel the signer signs for is the channel the lock is read on.
64
+ const channel = evidenceChannelOfSigner(options.signer, options.schemas);
62
65
  const resolution = {
63
66
  distributionId: options.distributionId,
64
67
  dataRoot: options.dataRoot,
@@ -66,6 +69,7 @@ async function deriveCompleteness(options, { outputRoot }) {
66
69
  schemas: options.schemas,
67
70
  expectedContractAuthority: options.expectedContractAuthority,
68
71
  registryClientFactory: options.registryClientFactory,
72
+ channel,
69
73
  };
70
74
  const staged = outputRoot === null
71
75
  ? await resolveProducerEvidence(resolution)
@@ -74,6 +78,7 @@ async function deriveCompleteness(options, { outputRoot }) {
74
78
  distributionLock: staged.distributionLock,
75
79
  imageLock: staged.imageLock,
76
80
  images: staged.images,
81
+ channel,
77
82
  releaseSequence: options.releaseSequence,
78
83
  releaseSourceCommit: options.releaseSourceCommit,
79
84
  runId: options.runId,
@@ -18,6 +18,7 @@ import {
18
18
  acceptSignedEvidenceStatement,
19
19
  assertPinnedCosignVersion,
20
20
  buildImageDeliveryEntitlementStatement,
21
+ evidenceChannelOfSigner,
21
22
  loadPinnedTrustPolicy,
22
23
  parseEvidenceCliFlags,
23
24
  stageEvidenceStatement,
@@ -26,10 +27,11 @@ import {
26
27
  WorkspaceRoot,
27
28
  configureEditionWorkspaceFromArgv,
28
29
  dataRoot,
30
+ editionsRoot,
29
31
  isEntryModule,
30
32
  } from './edition-workspace.mjs';
31
33
  import { ENTITLEMENT_DESCRIPTOR_FILE, entitlementDescriptorDocument } from './image-delivery-entitlement-descriptor.mjs';
32
- import { canonicalJsonBytes, readCanonicalJsonFromRoot } from './image-evidence.mjs';
34
+ import { canonicalJsonBytes, readCanonicalJsonFromRoot, readEditionDistributionLock } from './image-evidence.mjs';
33
35
  import { serializeJson } from './lib.mjs';
34
36
  import { githubWorkflowSigner } from './produce-distribution-completeness-evidence.mjs';
35
37
  import {
@@ -81,6 +83,12 @@ function deriveEntitlement(options) {
81
83
  schemas: options.schemas,
82
84
  });
83
85
  const statement = buildImageDeliveryEntitlementStatement({
86
+ distributionLock: readEditionDistributionLock(
87
+ options.editionsRoot,
88
+ options.distributionId,
89
+ options.schemas,
90
+ evidenceChannelOfSigner(options.signer, options.schemas),
91
+ ),
84
92
  distributionId: options.distributionId,
85
93
  completenessStatementDigest,
86
94
  customerId: options.customerId,
@@ -226,6 +234,7 @@ const SHARED_FLAGS = Object.freeze([
226
234
  async function main() {
227
235
  const [phase, ...args] = configureEditionWorkspaceFromArgv(process.argv.slice(2), [
228
236
  WorkspaceRoot.Data,
237
+ WorkspaceRoot.Editions,
229
238
  WorkspaceRoot.Kernel,
230
239
  ]);
231
240
  if (phase !== 'stage' && phase !== 'accept') {
@@ -240,6 +249,7 @@ async function main() {
240
249
  const options = {
241
250
  distributionId: flags.require('distribution'),
242
251
  evidenceRoot: resolve(flags.require('evidence-root')),
252
+ editionsRoot: editionsRoot(),
243
253
  stagingDirectory: resolve(flags.require('staging')),
244
254
  schemas,
245
255
  policy: loadPinnedTrustPolicy({ dataRoot: dataRoot(), schemas }),
@@ -5,14 +5,15 @@ import { readFileSync } from 'node:fs';
5
5
  import { biomeSourceTreeIds } from './biome-content.mjs';
6
6
  import { buildCatalogue } from './catalogue.mjs';
7
7
  import { EditionChannel } from './edition-constants.mjs';
8
- import { biomeIndexPath, platformServiceIndexPath, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
8
+ import { biomeIndexPath, kernelPackageDir, platformServiceIndexPath, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
9
+ import { kernelPackageOfAuthority } from './kernel-package.mjs';
9
10
  import { loadKernelDistributionSchemas } from './lib.mjs';
10
11
  import { loadEditionManifest } from './resolve-distribution.mjs';
11
12
  import { externalSourceTrees, sourceTreeLocator } from './resolve-images.mjs';
12
13
  import { committedRepositoryPins, createPinnedSourceAuthority } from './source-authority.mjs';
13
14
 
14
15
  /** The catalogue of release offering `id`, from the committed locks (`locks` as `readCommittedEdition` returns them). */
15
- export async function releaseCatalogue(id, locks) {
16
+ export async function releaseCatalogue(id, locks, { fetchKernelTarball } = {}) {
16
17
  const schemas = await loadKernelDistributionSchemas();
17
18
  const authority = createPinnedSourceAuthority({ pins: committedRepositoryPins() });
18
19
  // The WRITER of the catalogue's tree ids: the real `git rev-parse <commit>:<path>` of every biome.
@@ -23,6 +24,7 @@ export async function releaseCatalogue(id, locks) {
23
24
  locate: sourceTreeLocator(externalSourceTrees(locks.distributionLock)),
24
25
  vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
25
26
  });
27
+ const contractAuthority = schemas.parseRuntimeImageSourceIndex(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8'))).contractAuthority;
26
28
  return buildCatalogue({
27
29
  sourceTreeIds,
28
30
  offeringId: id,
@@ -32,7 +34,12 @@ export async function releaseCatalogue(id, locks) {
32
34
  chartSetLock: locks.chartSetLock,
33
35
  biomeIndex: JSON.parse(readFileSync(biomeIndexPath(), 'utf8')),
34
36
  platformServiceIndex: schemas.parsePlatformServiceSourceIndex(JSON.parse(readFileSync(platformServiceIndexPath(), 'utf8'))),
35
- contractAuthority: schemas.parseRuntimeImageSourceIndex(JSON.parse(readFileSync(runtimeImageSourceIndexPath(), 'utf8'))).contractAuthority,
37
+ contractAuthority,
38
+ kernel: await kernelPackageOfAuthority({
39
+ kernelPackageDir: kernelPackageDir(),
40
+ contractAuthority,
41
+ ...(fetchKernelTarball === undefined ? {} : { fetchTarball: fetchKernelTarball }),
42
+ }),
36
43
  leaf: loadEditionManifest(schemas.parseDistribution, id),
37
44
  });
38
45
  }
@@ -82,7 +82,7 @@ import {
82
82
  } from './registry-credentials.mjs';
83
83
 
84
84
  if (isEntryModule(import.meta.url)) {
85
- configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Deploy, WorkspaceRoot.Kernel]);
85
+ configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Kernel], { refuseOthers: true });
86
86
  }
87
87
 
88
88
  export const CHART_SET_LOCK_FILE = 'chart-set.lock.json';
@@ -18,6 +18,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { isDeepStrictEqual } from 'node:util';
20
20
 
21
+ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest } from './adapter-bundled-versions.mjs';
21
22
  import { AUTHORIZATION_LOCK_FILENAME, writeAuthorizationLock } from './authorization-base.mjs';
22
23
  import { CATALOGUE_FILE, CatalogueError } from './catalogue.mjs';
23
24
  import {
@@ -29,8 +30,8 @@ import {
29
30
  import { assertRosteredChartSetLock } from './edition-chart-set.mjs';
30
31
  import { EditionLockFile } from './edition-constants.mjs';
31
32
  import { editionsRoot } from './edition-workspace.mjs';
32
- import { validateKernelContractAuthorityFromGit } from './kernel-contract-authority.mjs';
33
- import { compareStrings, loadKernelDistributionSchemas, serializeJson, validateImmutableImageLock } from './lib.mjs';
33
+ import { adoptCatalogueKernel } from './kernel-package.mjs';
34
+ import { compareStrings, serializeJson, validateImmutableImageLock } from './lib.mjs';
34
35
  import { pinnedSource, snapshotPinSource } from './pin-source.mjs';
35
36
  import { composeAuthorizationBase, composeManifest, resolveDistributionWithIndex, UnknownSelectionError } from './resolve-distribution.mjs';
36
37
  import { resolveImageRows } from './resolve-images.mjs';
@@ -63,11 +64,22 @@ function catalogueCeiling(catalogue, schemas) {
63
64
  return { trustPolicy: read('trustPolicy', schemas.DistributionTrustPolicySchema), trustCeiling: read('trustCeiling', schemas.ProcessTrustPolicySchema) };
64
65
  }
65
66
 
67
+ /** A catalogue whose ceiling states no trust-domain assignments reserves no domain: the kernel's required ceiling argument is the empty policy. */
68
+ const NO_CATALOGUE_TRUST_CEILING = Object.freeze({ assignments: [] });
69
+
70
+ /** The kernel's `customerLockCeilingIssues` is the one check of a customer lock against the catalogue's ceiling. */
71
+ function assertLockWithinCatalogueCeiling({ catalogue, definition, lock, schemas }) {
72
+ const issues = schemas.customerLockCeilingIssues(catalogueCeiling(catalogue, schemas).trustCeiling, lock.biomes);
73
+ if (issues.length > 0) {
74
+ throw new CatalogueCeilingError(`catalogue "${catalogue.offeringId}" (${catalogue.channel}): "${definition.id}" is outside the catalogue's trustCeiling: ${issues.join('; ')}`);
75
+ }
76
+ }
77
+
66
78
  /**
67
79
  * The ceiling policy the customer's lock is written under: the catalogue's assignments, then
68
80
  * the definition's for what the catalogue leaves open. An assignment of the definition that
69
81
  * names a domain the catalogue assigns, or a biome the catalogue places, is not carried: the
70
- * catalogue's reading stands, and `assertWithinCatalogueCeiling` refuses the difference.
82
+ * catalogue's reading stands, and the kernel's `customerLockCeilingIssues` refuses the difference in the lock.
71
83
  */
72
84
  function ceilingUnderCatalogue(catalogueTrustCeiling, definitionTrustCeiling) {
73
85
  if (catalogueTrustCeiling === undefined) {return definitionTrustCeiling;}
@@ -80,25 +92,6 @@ function ceilingUnderCatalogue(catalogueTrustCeiling, definitionTrustCeiling) {
80
92
  return { assignments: [...catalogueTrustCeiling.assignments, ...open] };
81
93
  }
82
94
 
83
- /**
84
- * Every customer-source biome sits in the domain its definition names, and that is the domain
85
- * the catalogue's ceiling admits for it: the lock row carries the kernel's derivation
86
- * (`deriveTrustCeiling`) under `ceilingUnderCatalogue`, so a definition that reaches into a
87
- * domain the catalogue assigns, or moves a biome the catalogue places, differs from it here.
88
- */
89
- function assertWithinCatalogueCeiling({ catalogue, definition, lock, schemas }) {
90
- const stated = new Map((definition.trustCeiling?.assignments ?? []).flatMap((assignment) => assignment.biomes.map((biome) => [biome, assignment.domain])));
91
- for (const biome of lock.biomes.filter((candidate) => candidate.source !== undefined)) {
92
- const admitted = biome.trustCeiling ?? schemas.defaultTrustDomain(biome.id, biome.origin);
93
- if (stated.get(biome.id) !== admitted) {
94
- throw new CatalogueCeilingError(
95
- `catalogue "${catalogue.offeringId}" (${catalogue.channel}): "${definition.id}" assigns biome "${biome.id}" to trust domain ` +
96
- `"${stated.get(biome.id)}", and the catalogue's trustCeiling admits "${admitted}" for it`,
97
- );
98
- }
99
- }
100
- }
101
-
102
95
  /** `{name -> {imageRegistry, imageNamePrefix}}` from repeated `<source>=<registry>/<prefix>` values; the prefix is the text after the last "/". */
103
96
  export function parseSourceImages(values) {
104
97
  const byName = new Map();
@@ -247,8 +240,10 @@ export async function resolveCustomerImages({ id, catalogue, lock, composed, sch
247
240
  if (ownBiomes.length > 0) {
248
241
  const ownLock = { ...lock, biomes: ownBiomes, platformServices: [] };
249
242
  const runtimeImageSourceIndex = {
243
+ schemaVersion: 2,
250
244
  contractAuthority: catalogue.contractAuthority,
251
245
  repositoryPins: [],
246
+ producerWorkflowPins: [],
252
247
  platformServiceBindings: [],
253
248
  supplementalSubjects: [],
254
249
  crossRepoBuildInputs: [],
@@ -278,14 +273,9 @@ export async function resolveCustomerImages({ id, catalogue, lock, composed, sch
278
273
  return merged;
279
274
  }
280
275
 
281
- /** The kernel at --kernel must carry the contract the catalogue's offering was resolved under. */
282
- export async function assertKernelAtCatalogueAuthority(catalogue, schemas) {
283
- await validateKernelContractAuthorityFromGit({ authority: catalogue.contractAuthority, schemas, requireHeadMatch: false });
284
- }
285
-
286
276
  /**
287
277
  * The distribution and authorization locks of the customer edition `rawDefinition`
288
- * against `catalogue`. `catalogueDigest` is the digest of `catalogue`, written into the lock. `verifyAuthority` and `materialize` are injectable for the test suites.
278
+ * against `catalogue`. `catalogueDigest` is the digest of `catalogue`, written into the lock. `adoptKernel` (the catalogue's kernel from npm, proved its contract authority; returns the kernel schemas) and `materialize` are injectable for the test suites.
289
279
  */
290
280
  export async function resolveCustomerDistribution({
291
281
  rawDefinition,
@@ -294,10 +284,11 @@ export async function resolveCustomerDistribution({
294
284
  sourceImages,
295
285
  pins,
296
286
  materialize,
297
- verifyAuthority = assertKernelAtCatalogueAuthority,
287
+ adoptKernel = adoptCatalogueKernel,
288
+ fetchManifest = fetchRegistryManifest,
298
289
  }) {
299
290
  if (catalogueDigest === undefined) {throw new CatalogueError('a customer resolve names the digest of its catalogue');}
300
- const schemas = await loadKernelDistributionSchemas();
291
+ const schemas = await adoptKernel(catalogue);
301
292
  const { definition, mirror } = parseCustomerDefinition(schemas, rawDefinition);
302
293
  const composed = composeCustomerEdition({ schemas, definition, mirror, catalogue, sourceImages, pins });
303
294
  const id = composed.leaf.id;
@@ -312,9 +303,14 @@ export async function resolveCustomerDistribution({
312
303
  throw error;
313
304
  }
314
305
  const { lock, index } = resolved;
315
- assertTrustCeilingAssignsCustomerBiomes(schemas, definition, lock.biomes.filter((biome) => biome.source !== undefined).map((biome) => biome.id));
316
- assertWithinCatalogueCeiling({ catalogue, definition, lock, schemas });
317
- await verifyAuthority(catalogue, schemas);
306
+ assertTrustCeilingAssignsCustomerBiomes(
307
+ schemas,
308
+ definition,
309
+ lock.biomes.filter((biome) => biome.source !== undefined).map((biome) => biome.id),
310
+ catalogueCeiling(catalogue, schemas).trustCeiling ?? NO_CATALOGUE_TRUST_CEILING,
311
+ );
312
+ assertLockWithinCatalogueCeiling({ catalogue, definition, lock, schemas });
313
+ await assertAdapterBundlesMatchHost({ lock, hostRow: adapterHostRow(catalogue.biomes), fetchManifest });
318
314
  const authorizationLock = await composeAuthorizationBase(id, lock, index);
319
315
  // A customer lock names the catalogue it was resolved against; seal and verify refuse any other.
320
316
  return { id, schemas, composed, inputs, lock: { ...lock, catalogueDigest }, authorizationLock };