@xemahq/xema 0.30.0 → 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 (35) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/dev/infra-preflight.js +1 -1
  5. package/dist/dev/infra-preflight.js.map +1 -1
  6. package/dist/edition/verify-image-evidence.bundle.mjs +2921 -2290
  7. package/dist/edition/verify-image-evidence.bundle.provenance.json +481 -415
  8. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  9. package/dist/runtime/runner/helm-cli.js.map +1 -1
  10. package/dist/runtime/store-submit.d.ts.map +1 -1
  11. package/dist/runtime/store-submit.js.map +1 -1
  12. package/edition/README.md +2 -3
  13. package/edition/assemble-producer-input.mjs +1 -1
  14. package/edition/biome-content.mjs +6 -0
  15. package/edition/build-platform-service-index.mjs +2 -0
  16. package/edition/build-runtime-image-source-index.mjs +21 -6
  17. package/edition/catalogue.mjs +5 -1
  18. package/edition/checkout-indexed-source-repositories.mjs +41 -6
  19. package/edition/dispatched-runtime-components.mjs +82 -0
  20. package/edition/edition-protocol.mjs +2 -2
  21. package/edition/external-source-walk.mjs +1 -1
  22. package/edition/image-evidence.mjs +1 -1
  23. package/edition/kernel-capability-domain.mjs +30 -0
  24. package/edition/lib.mjs +119 -3
  25. package/edition/operation-grant-admission.mjs +70 -0
  26. package/edition/operation-requirements.mjs +128 -0
  27. package/edition/platform-lock-readability.mjs +2 -2
  28. package/edition/platform-service-class.mjs +42 -0
  29. package/edition/platform-service-ownership.mjs +78 -0
  30. package/edition/remedy-command.mjs +2 -2
  31. package/edition/resolve-customer-edition.mjs +4 -1
  32. package/edition/resolve-distribution.mjs +65 -15
  33. package/edition/resolve-images.mjs +65 -4
  34. package/edition/seal-image-digests.mjs +1 -1
  35. package/package.json +4 -4
package/edition/lib.mjs CHANGED
@@ -30,6 +30,12 @@ import {
30
30
  sourcePath,
31
31
  } from './edition-workspace.mjs';
32
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';
33
39
  import {
34
40
  createSourceAuthority,
35
41
  DistributionSourceMode,
@@ -768,6 +774,8 @@ export async function loadKernelDistributionSchemas() {
768
774
  describeHop,
769
775
  deriveWorkflowSubjects,
770
776
  distributionImagesPredicateChannelRefusal,
777
+ admitOperationGrants,
778
+ admitOrganizationSweeps,
771
779
  } = mod;
772
780
  let topology;
773
781
  try {
@@ -860,6 +868,10 @@ export async function loadKernelDistributionSchemas() {
860
868
  // Predicate v2 checks (`@xemahq/contracts` >= 0.78.0): the channel the verifying policy row states and
861
869
  // the lock digest the predicates bind.
862
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' ||
863
875
  typeof distributionImagesPredicateLockRefusal !== 'function' ||
864
876
  typeof distributionLockDigest !== 'function' ||
865
877
  typeof imageDeliveryEntitlementRefusal !== 'function' ||
@@ -879,7 +891,7 @@ export async function loadKernelDistributionSchemas() {
879
891
  `resolveRuntimeImagePlatforms/RuntimeImagePlatformKey/` +
880
892
  `kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
881
893
  `positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
882
- `deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/` +
894
+ `deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/` +
883
895
  `defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/` +
884
896
  `resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
885
897
  `BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
@@ -956,6 +968,8 @@ export async function loadKernelDistributionSchemas() {
956
968
  DistributionTrustPolicySchema,
957
969
  distributionLockDigest,
958
970
  distributionImagesPredicateChannelRefusal,
971
+ admitOperationGrants,
972
+ admitOrganizationSweeps,
959
973
  distributionImagesPredicateLockRefusal,
960
974
  imageDeliveryEntitlementRefusal,
961
975
  producerWorkflowPinRefusal,
@@ -1557,6 +1571,9 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1557
1571
  'resolve no worker image: a worker declares artifact.kind "oci-image" or "shared-oci-image"',
1558
1572
  );
1559
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);
1560
1577
  // A biome must declare its images in EXACTLY ONE shape. Declaring both a
1561
1578
  // legacy `ships.apis[]` entry and a v5 image component is ambiguous during
1562
1579
  // the staged migration — fail fast instead of guessing.
@@ -1580,6 +1597,12 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1580
1597
  // roster a release for them — so they are carried in a SEPARATE array and
1581
1598
  // contribute NO service-dependency edge and NO service owner.
1582
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 = {};
1583
1606
  // Infrastructure resources each rostered component NEEDS
1584
1607
  // (`protocol.resources`) and the resource kinds it PROVIDES
1585
1608
  // (`protocol.providesResources`). Declared INTENT only: the resolver binds
@@ -1591,6 +1614,14 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1591
1614
  // `service → what sharing a process needs to know about it`, for EVERY
1592
1615
  // platform-scheduled image component — see `componentProcessFacts`.
1593
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 = {};
1594
1625
  // Nexus services this biome's workers PROVIDE and REQUIRE (worker protocol
1595
1626
  // v2). Declared intent only, exactly like resources: the edition resolver
1596
1627
  // binds each requirement to the ONE shipped provider of the same
@@ -1682,7 +1713,19 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1682
1713
  `cannot carry a service-dependency edge`,
1683
1714
  );
1684
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
+ }
1685
1727
  runtimeComponents.push(name);
1728
+ runtimeComponentKeys[component.key] = name;
1686
1729
  continue;
1687
1730
  }
1688
1731
  if (component.scheduler !== 'platform') {
@@ -1764,6 +1807,10 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1764
1807
  nexusRequires.push(...nexus.requires);
1765
1808
  services.push(name);
1766
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;}
1767
1814
  const queues = componentWorkerTaskQueues(component, protocol, where);
1768
1815
  if (queues !== null) {workerTaskQueues[name] = queues;}
1769
1816
  const clients = componentWorkflowClients(protocol, where);
@@ -1929,6 +1976,25 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1929
1976
  }
1930
1977
  // Already de-duplicated and sorted by the kernel helper.
1931
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
+ );
1932
1998
 
1933
1999
  // Optional server biomes a web biome can integrate with but does not require
1934
2000
  // (never cause an ORPHANED resolution). Carried separately from `requires`.
@@ -1951,10 +2017,10 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
1951
2017
  ? [...new Set(xema.ownsCapabilityDomains)].sort(compareStrings)
1952
2018
  : [];
1953
2019
  for (const domain of ownsCapabilityDomains) {
1954
- if (typeof domain !== 'string' || !/^[a-z][a-z0-9-]*$/.test(domain)) {
2020
+ if (!isKernelCapabilityDomain(domain)) {
1955
2021
  throw new Error(
1956
2022
  `${rel}: xema.ownsCapabilityDomains contains ${JSON.stringify(domain)}, ` +
1957
- `which is not a valid capability domain (^[a-z][a-z0-9-]*$)`,
2023
+ `which is not a valid capability domain`,
1958
2024
  );
1959
2025
  }
1960
2026
  }
@@ -2069,6 +2135,17 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2069
2135
  }
2070
2136
  : {}),
2071
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 } : {}),
2072
2149
  hostModules,
2073
2150
  // Omitted when the biome declares none, or when its host modules already
2074
2151
  // carry it — see the projection above. At `origin/develop` that is every
@@ -2161,6 +2238,28 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
2161
2238
  ),
2162
2239
  }
2163
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
+ : {}),
2164
2263
  // The biome's signed dependency contract — DERIVED by the builder from the
2165
2264
  // SDK seams its code calls, read from the committed generated index and
2166
2265
  // validated there by the kernel schema. The edge set positive closure
@@ -2625,6 +2724,23 @@ export function validateImmutableBiomeIndex(index) {
2625
2724
  }
2626
2725
  runtimeComponents.add(component);
2627
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
+ }
2628
2744
  const edges = new Set();
2629
2745
  for (const edge of biome.serviceDependencies ?? []) {
2630
2746
  if (!services.has(edge.consumerService)) {
@@ -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
+ }
@@ -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) };
@@ -0,0 +1,78 @@
1
+ import { sourcePath } from './edition-workspace.mjs';
2
+ import { readGitJsonAtCommit, serializeJson, sha256Hex } from './lib.mjs';
3
+ import { sourceOwnedCapabilityDomains } from './platform-service-class.mjs';
4
+
5
+ /** Why a platform service's capability-domain ownership was refused. */
6
+ export const PlatformServiceOwnershipFault = Object.freeze({
7
+ /** The pinned source manifest could not be read. */
8
+ ManifestUnavailable: 'manifest-unavailable',
9
+ /** The manifest bytes do not hash to the index entry's `sourceManifestHash`. */
10
+ ManifestHashMismatch: 'manifest-hash-mismatch',
11
+ /** The index entry's `ownsCapabilityDomains` differs from the source manifest's. */
12
+ OwnershipMismatch: 'ownership-mismatch',
13
+ });
14
+
15
+ export class PlatformServiceOwnershipError extends Error {
16
+ constructor(fault, message) {
17
+ super(`[${fault}] ${message}`);
18
+ this.name = 'PlatformServiceOwnershipError';
19
+ this.fault = fault;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * The source manifest of one index entry, at the commit the entry pins. The
25
+ * entry's `path` is workspace-relative (`<repo>/<manifestPath>`).
26
+ */
27
+ export function readPinnedSourceManifest(entry) {
28
+ const manifestPath = entry.path.slice(`${entry.repo}/`.length);
29
+ try {
30
+ return readGitJsonAtCommit(sourcePath(entry.repo), manifestPath, entry.sourceCommit);
31
+ } catch (cause) {
32
+ throw new PlatformServiceOwnershipError(
33
+ PlatformServiceOwnershipFault.ManifestUnavailable,
34
+ `platform service "${entry.name}": cannot read ${entry.path} at ${entry.sourceCommit} ` +
35
+ `(${cause.message}); a resolve that reads the platform-service index needs the pinned sources`,
36
+ );
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Re-derive each platform service's `ownsCapabilityDomains` from its pinned
42
+ * source manifest and refuse the index when an entry disagrees. The index is a
43
+ * hand-editable file; the SERVICE's own manifest is the authority for which
44
+ * service owns a domain, and so for where grants land.
45
+ *
46
+ * Returns the index rows with the RE-DERIVED value, never the index's.
47
+ *
48
+ * @param {{ services: object[] }} index
49
+ * @param {{ names?: Set<string>, readManifest?: (entry: object) => unknown }} [options]
50
+ * `names` restricts the check to those services; `readManifest` is the test seam.
51
+ */
52
+ export function verifiedPlatformServiceIndex(index, { names, readManifest = readPinnedSourceManifest } = {}) {
53
+ const services = index.services.map((entry) => {
54
+ if (names !== undefined && !names.has(entry.name)) {return entry;}
55
+ const manifest = readManifest(entry);
56
+ const actualHash = sha256Hex(serializeJson(manifest));
57
+ if (actualHash !== entry.sourceManifestHash) {
58
+ throw new PlatformServiceOwnershipError(
59
+ PlatformServiceOwnershipFault.ManifestHashMismatch,
60
+ `platform service "${entry.name}": ${entry.path} at ${entry.sourceCommit} hashes to ${actualHash}, ` +
61
+ `but the index pins sourceManifestHash ${entry.sourceManifestHash}`,
62
+ );
63
+ }
64
+ const derived = sourceOwnedCapabilityDomains(manifest, `${entry.path} at ${entry.sourceCommit}`);
65
+ const claimed = entry.ownsCapabilityDomains;
66
+ if (serializeJson(claimed ?? null) !== serializeJson(derived.ownsCapabilityDomains ?? null)) {
67
+ throw new PlatformServiceOwnershipError(
68
+ PlatformServiceOwnershipFault.OwnershipMismatch,
69
+ `platform service "${entry.name}": the index claims ownsCapabilityDomains ${JSON.stringify(claimed ?? [])}, ` +
70
+ `its source manifest declares ${JSON.stringify(derived.ownsCapabilityDomains ?? [])} ` +
71
+ `(sourceManifestHash ${entry.sourceManifestHash})`,
72
+ );
73
+ }
74
+ const { ownsCapabilityDomains: _claimed, ...rest } = entry;
75
+ return { ...rest, ...derived };
76
+ });
77
+ return { ...index, services };
78
+ }
@@ -10,7 +10,7 @@
10
10
  // cannot run — and the reader of a dist-check failure is standing in a FLAT
11
11
  // checkout of this repository, which is exactly where the pnpm spelling does
12
12
  // not resolve. A remedy that cannot be run where it is printed is a dead end
13
- // (`.claude/rules/writing-boundary-checks.md`, and closure-ledger A25).
13
+ // (`.claude/rules/writing-boundary-checks.md`).
14
14
  //
15
15
  // Measured 2026-09-28 at `origin/develop`: 52 `pnpm dist:*` references in
16
16
  // this repository, 19 such scripts declared in the aggregator, and ZERO
@@ -21,7 +21,7 @@
21
21
  // That was the obvious fix and it is the wrong one: it would put 19 name→file
22
22
  // mappings in two places, free to drift, and drift between them is invisible
23
23
  // — a new script reachable from one side and not the other, with nothing able
24
- // to say so short of a new gate. Closure-ledger A26 forbids buying a fix with
24
+ // to say so short of a new gate. The rule forbids buying a fix with
25
25
  // a permanent check.
26
26
  //
27
27
  // Deriving the command from the script's REAL PATH has one authority — the
@@ -292,7 +292,10 @@ export async function resolveCustomerDistribution({
292
292
  const { definition, mirror } = parseCustomerDefinition(schemas, rawDefinition);
293
293
  const composed = composeCustomerEdition({ schemas, definition, mirror, catalogue, sourceImages, pins });
294
294
  const id = composed.leaf.id;
295
- const inputs = { composed, biomeIndex: catalogue.biomes, platformServiceIndex: { schemaVersion: 1, services: catalogue.platformServices } };
295
+ // `sourcesAvailable: false`: a customer machine holds no vendor source checkouts, so the platform
296
+ // services' ownership cannot be re-derived here. It is verified where the catalogue is produced
297
+ // (`buildCatalogue`), and the catalogue is digest-pinned.
298
+ const inputs = { composed, biomeIndex: catalogue.biomes, platformServiceIndex: { schemaVersion: 1, services: catalogue.platformServices }, sourcesAvailable: false };
296
299
  let resolved;
297
300
  try {
298
301
  resolved = await resolveDistributionWithIndex(id, { inputs, ...(materialize === undefined ? {} : { materialize }) });