@xemahq/xema 0.29.1 → 0.31.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 (62) hide show
  1. package/README.md +60 -389
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/edition.js +3 -3
  6. package/dist/commands/edition.js.map +1 -1
  7. package/dist/dev/infra-preflight.js +1 -1
  8. package/dist/dev/infra-preflight.js.map +1 -1
  9. package/dist/edition/verify-image-evidence.bundle.mjs +10030 -1491
  10. package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
  11. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  12. package/dist/runtime/runner/helm-cli.js.map +1 -1
  13. package/dist/runtime/store-submit.d.ts.map +1 -1
  14. package/dist/runtime/store-submit.js.map +1 -1
  15. package/edition/README.md +50 -271
  16. package/edition/adapter-bundled-versions.mjs +107 -0
  17. package/edition/adapter-host-versions.mjs +41 -0
  18. package/edition/assemble-producer-input.mjs +1 -1
  19. package/edition/authorization-base.mjs +18 -9
  20. package/edition/biome-content.mjs +6 -0
  21. package/edition/build-platform-service-index.mjs +2 -0
  22. package/edition/build-runtime-image-source-index.mjs +97 -9
  23. package/edition/catalogue.mjs +20 -13
  24. package/edition/checkout-indexed-source-repositories.mjs +41 -6
  25. package/edition/customer-definition.mjs +3 -20
  26. package/edition/develop-customer-edition.mjs +2 -2
  27. package/edition/develop-edition-resolve.mjs +10 -2
  28. package/edition/develop-edition.mjs +56 -62
  29. package/edition/develop-freeze.mjs +14 -3
  30. package/edition/dispatched-runtime-components.mjs +82 -0
  31. package/edition/dist-check.mjs +1 -0
  32. package/edition/distribution-evidence-set.mjs +32 -5
  33. package/edition/edition-channel.mjs +15 -1
  34. package/edition/edition-locks.mjs +16 -6
  35. package/edition/edition-protocol.mjs +30 -8
  36. package/edition/edition-workspace.mjs +8 -0
  37. package/edition/external-source-walk.mjs +1 -1
  38. package/edition/image-evidence.mjs +134 -26
  39. package/edition/kernel-capability-domain.mjs +30 -0
  40. package/edition/kernel-package.mjs +193 -0
  41. package/edition/lib.mjs +192 -4
  42. package/edition/oci-evidence-verbs.mjs +2 -2
  43. package/edition/operation-grant-admission.mjs +70 -0
  44. package/edition/operation-requirements.mjs +128 -0
  45. package/edition/plan-image-evidence-refresh.mjs +2 -2
  46. package/edition/platform-lock-readability.mjs +2 -2
  47. package/edition/platform-service-class.mjs +42 -0
  48. package/edition/platform-service-ownership.mjs +78 -0
  49. package/edition/produce-distribution-completeness-evidence.mjs +5 -0
  50. package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
  51. package/edition/release-catalogue.mjs +10 -3
  52. package/edition/remedy-command.mjs +2 -2
  53. package/edition/resolve-chart-set.mjs +1 -1
  54. package/edition/resolve-customer-edition.mjs +33 -34
  55. package/edition/resolve-distribution.mjs +70 -16
  56. package/edition/resolve-edition.mjs +3 -2
  57. package/edition/resolve-images.mjs +65 -4
  58. package/edition/seal-image-digests.mjs +17 -5
  59. package/edition/trust-policy-arming-verb.mjs +2 -10
  60. package/edition/verify-image-evidence.mjs +1 -0
  61. package/examples/README.md +3 -2
  62. package/package.json +4 -4
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,
@@ -27,6 +30,12 @@ import {
27
30
  sourcePath,
28
31
  } from './edition-workspace.mjs';
29
32
  import { SHA256_HEX_PATTERN } from './evidence-grammars.mjs';
33
+ import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
34
+ import {
35
+ assertPrivilegesHavePrincipals,
36
+ componentOperationRequirements,
37
+ componentOrganizationSweepRequirements,
38
+ } from './operation-requirements.mjs';
30
39
  import {
31
40
  createSourceAuthority,
32
41
  DistributionSourceMode,
@@ -688,6 +697,18 @@ export async function loadKernelDistributionSchemas() {
688
697
  `Underlying error: ${err.message}`,
689
698
  );
690
699
  }
700
+ // The Node-only half of the distribution contracts (`distribution/server`): the lock digest and the
701
+ // predicate checks that recompute it.
702
+ let server;
703
+ try {
704
+ server = await import(`file://${kernelDistPath('distribution', 'server', 'index.js')}`);
705
+ } catch (err) {
706
+ throw new Error(
707
+ `Cannot load the built kernel distribution/server module from ${kernelDistPath('distribution', 'server', 'index.js')}. ` +
708
+ `Rebuild the kernel checkout (${kernelRepoDir()}). Underlying error: ${err.message}`,
709
+ );
710
+ }
711
+ const { distributionLockDigest, distributionImagesPredicateLockRefusal, imageDeliveryEntitlementRefusal } = server;
691
712
  const {
692
713
  parseDistribution,
693
714
  parseDistributionLock,
@@ -695,6 +716,8 @@ export async function loadKernelDistributionSchemas() {
695
716
  parseUnsealedDistributionLock,
696
717
  parseCustomerEditionDefinition,
697
718
  customerTrustCeilingIssues,
719
+ customerLockCeilingIssues,
720
+ biomeOriginsOfLock,
698
721
  sealDistributionLock,
699
722
  unsealedProjection,
700
723
  UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
@@ -714,6 +737,7 @@ export async function loadKernelDistributionSchemas() {
714
737
  SigstoreEvidenceDescriptorSchema,
715
738
  parsePlatformServiceSourceIndex,
716
739
  parseRuntimeImageSourceIndex,
740
+ producerWorkflowPinRefusal,
717
741
  resolveRuntimeImagePlatforms,
718
742
  BiomeInstallPolicy,
719
743
  BiomeLaunchMode,
@@ -749,6 +773,9 @@ export async function loadKernelDistributionSchemas() {
749
773
  describeProvenance,
750
774
  describeHop,
751
775
  deriveWorkflowSubjects,
776
+ distributionImagesPredicateChannelRefusal,
777
+ admitOperationGrants,
778
+ admitOrganizationSweeps,
752
779
  } = mod;
753
780
  let topology;
754
781
  try {
@@ -791,6 +818,7 @@ export async function loadKernelDistributionSchemas() {
791
818
  typeof SigstoreEvidenceDescriptorSchema?.parse !== 'function' ||
792
819
  typeof parsePlatformServiceSourceIndex !== 'function' ||
793
820
  typeof parseRuntimeImageSourceIndex !== 'function' ||
821
+ typeof producerWorkflowPinRefusal !== 'function' ||
794
822
  typeof resolveRuntimeImagePlatforms !== 'function' ||
795
823
  typeof resolveFirstPartyCapabilityDomains !== 'function' ||
796
824
  typeof DistributionVisibility !== 'object' ||
@@ -837,6 +865,16 @@ export async function loadKernelDistributionSchemas() {
837
865
  // A biome's workflow subjects and its edition trust ceiling, each written
838
866
  // through the kernel's one derivation (`@xemahq/contracts` >= 0.71.0).
839
867
  typeof deriveWorkflowSubjects !== 'function' ||
868
+ // Predicate v2 checks (`@xemahq/contracts` >= 0.78.0): the channel the verifying policy row states and
869
+ // the lock digest the predicates bind.
870
+ typeof distributionImagesPredicateChannelRefusal !== 'function' ||
871
+ // The ONE admission of service-to-service operation grants; the seal and
872
+ // the resolve refuse through it (`assertOperationGrantsAdmitted`).
873
+ typeof admitOperationGrants !== 'function' ||
874
+ typeof admitOrganizationSweeps !== 'function' ||
875
+ typeof distributionImagesPredicateLockRefusal !== 'function' ||
876
+ typeof distributionLockDigest !== 'function' ||
877
+ typeof imageDeliveryEntitlementRefusal !== 'function' ||
840
878
  typeof deriveTrustCeiling !== 'function' ||
841
879
  // The trust-domain vocabulary a customer edition is bounded by: the vendor's
842
880
  // own domain, a biome's default domain, and the two ceiling grammars a
@@ -853,7 +891,7 @@ export async function loadKernelDistributionSchemas() {
853
891
  `resolveRuntimeImagePlatforms/RuntimeImagePlatformKey/` +
854
892
  `kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
855
893
  `positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
856
- `deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/` +
894
+ `deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/` +
857
895
  `defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/` +
858
896
  `resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
859
897
  `BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
@@ -867,6 +905,8 @@ export async function loadKernelDistributionSchemas() {
867
905
  parseUnsealedDistributionLock,
868
906
  parseCustomerEditionDefinition,
869
907
  customerTrustCeilingIssues,
908
+ customerLockCeilingIssues,
909
+ biomeOriginsOfLock,
870
910
  sealDistributionLock,
871
911
  unsealedProjection,
872
912
  UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
@@ -926,6 +966,13 @@ export async function loadKernelDistributionSchemas() {
926
966
  FIRST_PARTY_TRUST_DOMAIN,
927
967
  ProcessTrustPolicySchema,
928
968
  DistributionTrustPolicySchema,
969
+ distributionLockDigest,
970
+ distributionImagesPredicateChannelRefusal,
971
+ admitOperationGrants,
972
+ admitOrganizationSweeps,
973
+ distributionImagesPredicateLockRefusal,
974
+ imageDeliveryEntitlementRefusal,
975
+ producerWorkflowPinRefusal,
929
976
  };
930
977
  }
931
978
 
@@ -1086,6 +1133,10 @@ function listBiomeManifestsAt(directory, repo, commit) {
1086
1133
  rel,
1087
1134
  repo,
1088
1135
  path,
1136
+ // The manifest's directory in its repository, and a reader of any file of
1137
+ // that repository at the SAME pinned commit (the host's pnpm-lock.yaml).
1138
+ manifestDirectory,
1139
+ readRepositoryText: (repoRelativePath) => readGitTextAtCommitOrNull(directory, repoRelativePath, commit),
1089
1140
  ...(authorizationModule === null ? {} : { authorizationModule }),
1090
1141
  });
1091
1142
  }
@@ -1520,6 +1571,9 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1520
1571
  'resolve no worker image: a worker declares artifact.kind "oci-image" or "shared-oci-image"',
1521
1572
  );
1522
1573
  }
1574
+ // A privilege (`requiresOperations`, `requiresOrganizationSweeps`) is admitted for a lock workload
1575
+ // principal only; on any other component it is refused, never dropped.
1576
+ assertPrivilegesHavePrincipals(components, imageComponents, rel);
1523
1577
  // A biome must declare its images in EXACTLY ONE shape. Declaring both a
1524
1578
  // legacy `ships.apis[]` entry and a v5 image component is ambiguous during
1525
1579
  // the staged migration — fail fast instead of guessing.
@@ -1543,6 +1597,12 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1543
1597
  // roster a release for them — so they are carried in a SEPARATE array and
1544
1598
  // contribute NO service-dependency edge and NO service owner.
1545
1599
  const runtimeComponents = [];
1600
+ // `component key → image name` for every runtime component declared through
1601
+ // `xema.components[]`. A `runtime-image` edge names its target by component
1602
+ // KEY, and the image lock by image NAME, so the edition resolver reads this
1603
+ // to follow another biome's edge to this one. The legacy `ships.apis[]`
1604
+ // shape declares no component key, so no edge can name its components.
1605
+ const runtimeComponentKeys = {};
1546
1606
  // Infrastructure resources each rostered component NEEDS
1547
1607
  // (`protocol.resources`) and the resource kinds it PROVIDES
1548
1608
  // (`protocol.providesResources`). Declared INTENT only: the resolver binds
@@ -1554,6 +1614,14 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1554
1614
  // `service → what sharing a process needs to know about it`, for EVERY
1555
1615
  // platform-scheduled image component — see `componentProcessFacts`.
1556
1616
  const serviceProcessFacts = {};
1617
+ // `component → the service-to-service operations it requires`
1618
+ // (`requiresOperations`), keyed by the SAME name the lock uses. Declared
1619
+ // intent only: the seal admits or refuses each one.
1620
+ const operationRequirements = {};
1621
+ // `component → the organization sweeps it may hold`
1622
+ // (`requiresOrganizationSweeps`), keyed like `operationRequirements`.
1623
+ // Declared intent only: the seal admits or refuses each one.
1624
+ const organizationSweepRequirements = {};
1557
1625
  // Nexus services this biome's workers PROVIDE and REQUIRE (worker protocol
1558
1626
  // v2). Declared intent only, exactly like resources: the edition resolver
1559
1627
  // binds each requirement to the ONE shipped provider of the same
@@ -1645,7 +1713,19 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1645
1713
  `cannot carry a service-dependency edge`,
1646
1714
  );
1647
1715
  }
1716
+ // An operation grant is admitted for a rostered workload caller, and a
1717
+ // runtime component is not one. Refused rather than dropped.
1718
+ for (const field of ['requiresOperations', 'requiresOrganizationSweeps']) {
1719
+ if (component[field] !== undefined) {
1720
+ throw new Error(
1721
+ `${where} declares scheduler "runtime" and ${field} — a ` +
1722
+ `privilege names a rostered workload, and a component that is ` +
1723
+ `never scheduled is not one`,
1724
+ );
1725
+ }
1726
+ }
1648
1727
  runtimeComponents.push(name);
1728
+ runtimeComponentKeys[component.key] = name;
1649
1729
  continue;
1650
1730
  }
1651
1731
  if (component.scheduler !== 'platform') {
@@ -1727,6 +1807,10 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1727
1807
  nexusRequires.push(...nexus.requires);
1728
1808
  services.push(name);
1729
1809
  serviceProcessFacts[name] = componentProcessFacts(component, where);
1810
+ const operations = componentOperationRequirements(component, where);
1811
+ if (operations !== null) {operationRequirements[name] = operations;}
1812
+ const sweeps = componentOrganizationSweepRequirements(component, where);
1813
+ if (sweeps !== null) {organizationSweepRequirements[name] = sweeps;}
1730
1814
  const queues = componentWorkerTaskQueues(component, protocol, where);
1731
1815
  if (queues !== null) {workerTaskQueues[name] = queues;}
1732
1816
  const clients = componentWorkflowClients(protocol, where);
@@ -1892,6 +1976,25 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1892
1976
  }
1893
1977
  // Already de-duplicated and sorted by the kernel helper.
1894
1978
  const requiresUnique = [...kernelBiome.requiredBiomeDependencyIds(parsedIndex.biome)];
1979
+ // The runtime components this biome's components dispatch: the kernel's
1980
+ // `runtime-image` edges, reduced to their distinct targets. "Needs that
1981
+ // image" and nothing else — the kernel keeps them out of
1982
+ // `requiredBiomeDependencyIds`, and they never enter `requires`: the edition
1983
+ // resolver carries the target's image without installing its biome.
1984
+ const dispatchesRuntimeComponents = [
1985
+ ...new Map(
1986
+ parsedIndex.biome.edges
1987
+ .filter((edge) => edge.kind === kernelBiome.BiomeGeneratedEdgeKind.RuntimeImage)
1988
+ .map((edge) => [
1989
+ `${edge.target.biomeId}\0${edge.target.componentKey}`,
1990
+ { biomeId: edge.target.biomeId, componentKey: edge.target.componentKey },
1991
+ ]),
1992
+ ).values(),
1993
+ ].sort(
1994
+ (a, b) =>
1995
+ compareStrings(a.biomeId, b.biomeId) ||
1996
+ compareStrings(a.componentKey, b.componentKey),
1997
+ );
1895
1998
 
1896
1999
  // Optional server biomes a web biome can integrate with but does not require
1897
2000
  // (never cause an ORPHANED resolution). Carried separately from `requires`.
@@ -1914,10 +2017,10 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1914
2017
  ? [...new Set(xema.ownsCapabilityDomains)].sort(compareStrings)
1915
2018
  : [];
1916
2019
  for (const domain of ownsCapabilityDomains) {
1917
- if (typeof domain !== 'string' || !/^[a-z][a-z0-9-]*$/.test(domain)) {
2020
+ if (!isKernelCapabilityDomain(domain)) {
1918
2021
  throw new Error(
1919
2022
  `${rel}: xema.ownsCapabilityDomains contains ${JSON.stringify(domain)}, ` +
1920
- `which is not a valid capability domain (^[a-z][a-z0-9-]*$)`,
2023
+ `which is not a valid capability domain`,
1921
2024
  );
1922
2025
  }
1923
2026
  }
@@ -2032,6 +2135,17 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2032
2135
  }
2033
2136
  : {}),
2034
2137
  runtimeComponents,
2138
+ // Omitted when the biome has no runtime component (almost every biome),
2139
+ // so the index stays byte-stable for everything that declares none.
2140
+ ...(Object.keys(runtimeComponentKeys).length > 0
2141
+ ? {
2142
+ runtimeComponentKeys: Object.fromEntries(
2143
+ Object.entries(runtimeComponentKeys).sort(([a], [b]) => compareStrings(a, b)),
2144
+ ),
2145
+ }
2146
+ : {}),
2147
+ // Omitted when the biome dispatches nothing, for the same reason.
2148
+ ...(dispatchesRuntimeComponents.length > 0 ? { dispatchesRuntimeComponents } : {}),
2035
2149
  hostModules,
2036
2150
  // Omitted when the biome declares none, or when its host modules already
2037
2151
  // carry it — see the projection above. At `origin/develop` that is every
@@ -2058,6 +2172,16 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2058
2172
  .filter((name) => typeof name === 'string' && name.length > 0),
2059
2173
  ),
2060
2174
  ].sort(compareStrings),
2175
+ // The capabilities the signed biome declares (a delegation
2176
+ // origination may lend only these): the manifest's
2177
+ // `xema.exposesCapabilities`; `requires*` are what it asks for.
2178
+ declaredCapabilities: [
2179
+ ...new Set(
2180
+ (Array.isArray(xema.exposesCapabilities) ? xema.exposesCapabilities : []).filter(
2181
+ (ref) => typeof ref === 'string' && ref.length > 0,
2182
+ ),
2183
+ ),
2184
+ ].sort(compareStrings),
2061
2185
  },
2062
2186
  },
2063
2187
  }
@@ -2114,6 +2238,28 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2114
2238
  ),
2115
2239
  }
2116
2240
  : {}),
2241
+ // The service-to-service operations each workload component requires;
2242
+ // omitted when none declares any, so the index stays byte-stable.
2243
+ ...(Object.keys(operationRequirements).length > 0
2244
+ ? {
2245
+ operationRequirements: Object.fromEntries(
2246
+ Object.entries(operationRequirements).sort(([a], [b]) =>
2247
+ compareStrings(a, b),
2248
+ ),
2249
+ ),
2250
+ }
2251
+ : {}),
2252
+ // The organization sweeps each workload component may hold; omitted when
2253
+ // none declares any.
2254
+ ...(Object.keys(organizationSweepRequirements).length > 0
2255
+ ? {
2256
+ organizationSweepRequirements: Object.fromEntries(
2257
+ Object.entries(organizationSweepRequirements).sort(([a], [b]) =>
2258
+ compareStrings(a, b),
2259
+ ),
2260
+ ),
2261
+ }
2262
+ : {}),
2117
2263
  // The biome's signed dependency contract — DERIVED by the builder from the
2118
2264
  // SDK seams its code calls, read from the committed generated index and
2119
2265
  // validated there by the kernel schema. The edge set positive closure
@@ -2263,6 +2409,26 @@ export function validateServiceDependencyOwnerParity(
2263
2409
  return entries;
2264
2410
  }
2265
2411
 
2412
+ /**
2413
+ * The adapter host's resolved `@xemahq/contracts` / `@xemahq/biome-sdk` versions, read from its source
2414
+ * repository's pnpm-lock.yaml at the pinned commit. An INDEX field only: the distribution lock never carries it.
2415
+ */
2416
+ function adapterHostPackageVersions(manifest) {
2417
+ const services = (manifest.raw.xema.components ?? []).filter(
2418
+ (component) => component?.kind === 'service' && component.artifact?.kind === 'oci-image',
2419
+ );
2420
+ if (services.length !== 1) {
2421
+ throw new Error(`${manifest.rel}: the adapter host declares ${services.length} oci-image service components; exactly one carries its package`);
2422
+ }
2423
+ const lockfilePath = 'pnpm-lock.yaml';
2424
+ return readHostPackageVersions({
2425
+ lockfileText: manifest.readRepositoryText(lockfilePath),
2426
+ importer: `${manifest.manifestDirectory}/${services[0].artifact.path}`,
2427
+ lockfilePath: `${manifest.repo}/${lockfilePath}`,
2428
+ parseYaml,
2429
+ });
2430
+ }
2431
+
2266
2432
  export async function buildBiomeIndex(
2267
2433
  platformServiceIndex,
2268
2434
  sourceRepositories = distributionBiomeSourceRepositories(),
@@ -2275,7 +2441,12 @@ export async function buildBiomeIndex(
2275
2441
  });
2276
2442
  const entries = [];
2277
2443
  for (const manifest of await committedBiomeManifests(sourceRepositories, authority)) {
2278
- entries.push(await manifestToIndexEntry(manifest));
2444
+ const entry = await manifestToIndexEntry(manifest);
2445
+ entries.push(
2446
+ entry.id === ADAPTER_HOST_BIOME_ID
2447
+ ? { ...entry, hostPackageVersions: adapterHostPackageVersions(manifest) }
2448
+ : entry,
2449
+ );
2279
2450
  }
2280
2451
  resolveServiceDependencyOwners(entries, platformServiceIndex);
2281
2452
  assertDeclaredDatabasesUnowned(entries);
@@ -2553,6 +2724,23 @@ export function validateImmutableBiomeIndex(index) {
2553
2724
  }
2554
2725
  runtimeComponents.add(component);
2555
2726
  }
2727
+ for (const [key, image] of Object.entries(biome.runtimeComponentKeys ?? {})) {
2728
+ if (!runtimeComponents.has(image)) {
2729
+ throw new Error(
2730
+ `biome "${biome.id}" keys runtime component "${key}" to "${String(image)}", which is not one of its runtime components`,
2731
+ );
2732
+ }
2733
+ }
2734
+ const dispatched = new Set();
2735
+ for (const edge of biome.dispatchesRuntimeComponents ?? []) {
2736
+ const edgeKey = `${edge.biomeId}\0${edge.componentKey}`;
2737
+ if (dispatched.has(edgeKey)) {
2738
+ throw new Error(
2739
+ `biome "${biome.id}" dispatches runtime component "${edge.componentKey}" of biome "${edge.biomeId}" more than once`,
2740
+ );
2741
+ }
2742
+ dispatched.add(edgeKey);
2743
+ }
2556
2744
  const edges = new Set();
2557
2745
  for (const edge of biome.serviceDependencies ?? []) {
2558
2746
  if (!services.has(edge.consumerService)) {
@@ -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) {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Admission of service-to-service operation grants at the lock.
3
+ *
4
+ * A manifest declares intent (`requiresOperations`, carried in the lock as
5
+ * `operationRequirements`); the kernel's `admitOperationGrants` admits or
6
+ * refuses every requirement, and a refusal is never a warning. The resolve
7
+ * and the seal both call {@link assertOperationGrantsAdmitted}, so a lock with
8
+ * a refused requirement is neither written nor sealed.
9
+ */
10
+
11
+ /** Thrown when one or more declared operation requirements are not admitted. */
12
+ export class OperationGrantAdmissionError extends Error {
13
+ /**
14
+ * @param {string} distributionId
15
+ * @param {ReadonlyArray<{code: string, biome: string, component: string, operation: string, message: string}>} refusals
16
+ */
17
+ constructor(distributionId, refusals) {
18
+ super(
19
+ `distribution "${distributionId}" declares operation requirements that are not admitted:\n` +
20
+ refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
21
+ );
22
+ this.name = 'OperationGrantAdmissionError';
23
+ this.distributionId = distributionId;
24
+ this.refusals = refusals;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Admit every operation requirement of `lock` through the kernel, or throw
30
+ * {@link OperationGrantAdmissionError} listing every refusal. Returns the
31
+ * admitted grants.
32
+ */
33
+ export function assertOperationGrantsAdmitted(schemas, lock) {
34
+ const { grants, refusals } = schemas.admitOperationGrants(lock);
35
+ if (refusals.length > 0) {
36
+ throw new OperationGrantAdmissionError(lock.distributionId, refusals);
37
+ }
38
+ return grants;
39
+ }
40
+
41
+ /** Thrown when one or more declared organization sweeps are not admitted. */
42
+ export class OrganizationSweepAdmissionError extends Error {
43
+ /**
44
+ * @param {string} distributionId
45
+ * @param {ReadonlyArray<{code: string, biome: string, component: string, sweep: string, message: string}>} refusals
46
+ */
47
+ constructor(distributionId, refusals) {
48
+ super(
49
+ `distribution "${distributionId}" declares organization sweeps that are not admitted:\n` +
50
+ refusals.map((refusal) => `[${refusal.code}] ${refusal.message}`).join('\n'),
51
+ );
52
+ this.name = 'OrganizationSweepAdmissionError';
53
+ this.distributionId = distributionId;
54
+ this.refusals = refusals;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Admit every organization sweep requirement of `lock` through the kernel
60
+ * (`admitOrganizationSweeps`), or throw {@link OrganizationSweepAdmissionError}
61
+ * listing every refusal. Returns the admitted rows. The resolve and the seal
62
+ * call it beside {@link assertOperationGrantsAdmitted}.
63
+ */
64
+ export function assertOrganizationSweepsAdmitted(schemas, lock) {
65
+ const { grants, refusals } = schemas.admitOrganizationSweeps(lock);
66
+ if (refusals.length > 0) {
67
+ throw new OrganizationSweepAdmissionError(lock.distributionId, refusals);
68
+ }
69
+ return grants;
70
+ }
@@ -0,0 +1,128 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ import { kernelDistPath } from './edition-workspace.mjs';
4
+ import { compareStrings } from './string-order.mjs';
5
+
6
+ const requireFromHere = createRequire(import.meta.url);
7
+
8
+ let kernelPrivilegeGrammars;
9
+
10
+ /**
11
+ * The kernel's ONE operation-id grammar (`OperationIdSchema`) and closed
12
+ * `PlatformSweep` vocabulary, from the distribution subpath: the only plane
13
+ * the edition protocol exposes to this library.
14
+ */
15
+ function loadKernelPrivilegeGrammars() {
16
+ if (kernelPrivilegeGrammars) {return kernelPrivilegeGrammars;}
17
+ const mod = requireFromHere(kernelDistPath('distribution', 'index.js'));
18
+ if (typeof mod.OperationIdSchema?.safeParse !== 'function' || typeof mod.PlatformSweepSchema?.safeParse !== 'function') {
19
+ throw new Error(
20
+ `Built kernel distribution module at ${kernelDistPath('distribution', 'index.js')} does not export ` +
21
+ 'OperationIdSchema and PlatformSweepSchema — the dist is stale; rebuild kernel-contracts at a commit that carries them.',
22
+ );
23
+ }
24
+ kernelPrivilegeGrammars = mod;
25
+ return mod;
26
+ }
27
+
28
+ /**
29
+ * One component's declared privilege list, as the index carries it: sorted,
30
+ * unique, every entry graded by the kernel schema. `null` when undeclared. An
31
+ * empty list, a repeat or an invalid entry REFUSES naming the manifest: this
32
+ * reads the RAW manifest, and the kernel's component schema is not applied on
33
+ * this path.
34
+ */
35
+ function declaredPrivilegeList(component, where, field, schema, what) {
36
+ const declared = component[field];
37
+ if (declared === undefined) {return null;}
38
+ if (!Array.isArray(declared) || declared.length === 0) {
39
+ throw new Error(
40
+ `${where} declares ${field} that is not a non-empty array of ${what}s — ` +
41
+ 'a list that names nothing states nothing, so omit the field',
42
+ );
43
+ }
44
+ const values = declared.map((value, position) => {
45
+ const parsed = schema.safeParse(value);
46
+ if (!parsed.success) {
47
+ throw new Error(
48
+ `${where} ${field}[${position}] ${JSON.stringify(value)} is not ${what === 'operation id' ? 'an' : 'a'} ${what}: ` +
49
+ parsed.error.issues.map((issue) => issue.message).join('; '),
50
+ );
51
+ }
52
+ return parsed.data;
53
+ });
54
+ const unique = [...new Set(values)];
55
+ if (unique.length !== values.length) {
56
+ const repeated = values.find((value, position) => values.indexOf(value) !== position);
57
+ throw new Error(`${where} ${field} names ${repeated} more than once`);
58
+ }
59
+ return unique.sort(compareStrings);
60
+ }
61
+
62
+ /**
63
+ * The service-to-service operations one workload component declares it needs
64
+ * (`requiresOperations`), graded by the kernel's one operation-id grammar.
65
+ * Declared INTENT only; the seal admits or refuses each (`admitOperationGrants`).
66
+ */
67
+ export function componentOperationRequirements(component, where) {
68
+ const { OperationIdSchema } = loadKernelPrivilegeGrammars();
69
+ return declaredPrivilegeList(component, where, 'requiresOperations', OperationIdSchema, 'operation id');
70
+ }
71
+
72
+ /**
73
+ * The organization sweeps one workload component declares it may hold
74
+ * (`requiresOrganizationSweeps`), each a member of the kernel's closed
75
+ * `PlatformSweep`. A sweep's reach is fixed by the kernel, never declared.
76
+ * Declared INTENT only; the seal admits or refuses each (`admitOrganizationSweeps`).
77
+ */
78
+ export function componentOrganizationSweepRequirements(component, where) {
79
+ const { PlatformSweepSchema } = loadKernelPrivilegeGrammars();
80
+ return declaredPrivilegeList(component, where, 'requiresOrganizationSweeps', PlatformSweepSchema, 'platform sweep');
81
+ }
82
+
83
+ /**
84
+ * The privilege declarations a component may carry only if it is a workload
85
+ * principal in the lock.
86
+ */
87
+ export const PRINCIPAL_PRIVILEGE_FIELDS = Object.freeze(['requiresOperations', 'requiresOrganizationSweeps']);
88
+
89
+ /**
90
+ * Thrown when a component that has no workload principal in the lock declares
91
+ * a privilege. A principal is a rostered image component: a service (under its
92
+ * `protocol.serviceName`) or a worker (under its key). A job, an imageless
93
+ * component or a content/web/adapter component is never one, so its
94
+ * declaration could never be admitted; it is refused rather than dropped.
95
+ */
96
+ export class PrivilegeWithoutPrincipalError extends Error {
97
+ /**
98
+ * @param {string} manifest the manifest path
99
+ * @param {string} component the component key
100
+ * @param {string} field the privilege field it declares
101
+ */
102
+ constructor(manifest, component, field) {
103
+ super(
104
+ `${manifest}: xema.components[] "${component}" declares ${field}, but it has no ` +
105
+ 'workload principal in the lock (only a service or worker image component is one), ' +
106
+ 'so the declaration could never be admitted',
107
+ );
108
+ this.name = 'PrivilegeWithoutPrincipalError';
109
+ this.manifest = manifest;
110
+ this.component = component;
111
+ this.field = field;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Refuse every privilege declaration on a component outside `principals`
117
+ * (the components that become lock workloads). Never drops one silently.
118
+ */
119
+ export function assertPrivilegesHavePrincipals(components, principals, manifest) {
120
+ for (const component of components) {
121
+ if (component === null || typeof component !== 'object' || principals.includes(component)) {continue;}
122
+ for (const field of PRINCIPAL_PRIVILEGE_FIELDS) {
123
+ if (component[field] !== undefined) {
124
+ throw new PrivilegeWithoutPrincipalError(manifest, String(component.key ?? component.name ?? '?'), field);
125
+ }
126
+ }
127
+ }
128
+ }
@@ -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,
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // An edition's sealed lock must be PARSEABLE BY THE CONTRACTS VERSION THE
7
7
  // IMAGES OF THAT EDITION ACTUALLY RESOLVE. Nothing asserted that, and on
8
- // 2026-09-17 the gap took out three presenca-dev services at once:
8
+ // 2026-09-17 the gap took out three a customer environment services at once:
9
9
  //
10
10
  // Invalid distribution.lock.json: [biomes.87.source] Unrecognized key:
11
11
  // "imageNamePrefix"
@@ -92,7 +92,7 @@
92
92
  // KNOWN LIMIT, stated rather than discovered: a subject whose `repo` has NO
93
93
  // entry in `repositoryPins` is outside this check, because the pin index is
94
94
  // where a commit to read the declaration AT comes from. A customer edition
95
- // has one today — `presenca-biomes`, a customer repository pinned elsewhere.
95
+ // has one today — `<customer>-biomes`, a customer repository pinned elsewhere.
96
96
  // It was outside this gate before the narrowing too, so nothing was lost here;
97
97
  // it is named so nobody reads the corpus line as covering it.
98
98
  //
@@ -1,3 +1,6 @@
1
+ import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
2
+ import { compareStrings } from './string-order.mjs';
3
+
1
4
  /**
2
5
  * A platform service's installation class, as the index carries it.
3
6
  *
@@ -12,6 +15,45 @@ export function declaredInstallationClass(declaration) {
12
15
  : { installationClass: declaration.installationClass };
13
16
  }
14
17
 
18
+ /**
19
+ * The capability domains a platform service OWNS, as the index carries them:
20
+ * sorted and non-empty, or no key at all.
21
+ *
22
+ * Ownership is the SERVICE's own statement, read from its source manifest
23
+ * (`xema.ownsCapabilityDomains` in the manifest `platformServiceSources` names,
24
+ * at the pinned commit), the same field a biome manifest uses. An edition or
25
+ * the data root never asserts it. The manifest is bound into the index by
26
+ * `sourceManifestHash`, so the claim cannot drift from the source. Uniqueness
27
+ * across the index is the kernel's index schema's to grade.
28
+ *
29
+ * @param {unknown} sourceManifest the parsed source manifest
30
+ * @param {string} where names the manifest in a refusal
31
+ */
32
+ export function sourceOwnedCapabilityDomains(sourceManifest, where) {
33
+ const domains = sourceManifest?.xema?.ownsCapabilityDomains;
34
+ if (domains === undefined) {return {};}
35
+ if (!Array.isArray(domains) || domains.length === 0) {
36
+ throw new Error(
37
+ `${where}: xema.ownsCapabilityDomains must be a non-empty array of capability domains; ` +
38
+ 'omit it to own none',
39
+ );
40
+ }
41
+ for (const domain of domains) {
42
+ if (!isKernelCapabilityDomain(domain)) {
43
+ throw new Error(
44
+ `${where}: xema.ownsCapabilityDomains contains ${JSON.stringify(domain)}, ` +
45
+ 'which is not a valid capability domain',
46
+ );
47
+ }
48
+ }
49
+ const sorted = [...domains].sort(compareStrings);
50
+ const repeated = sorted.find((domain, index) => sorted[index - 1] === domain);
51
+ if (repeated !== undefined) {
52
+ throw new Error(`${where}: xema.ownsCapabilityDomains names "${repeated}" more than once`);
53
+ }
54
+ return { ownsCapabilityDomains: sorted };
55
+ }
56
+
15
57
  /** The (name, tier, declared class) a declaration and an index entry must agree on. */
16
58
  export function comparablePlatformService(entry) {
17
59
  return { name: entry.name, tier: entry.tier, ...declaredInstallationClass(entry) };