@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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/dev/infra-preflight.js +1 -1
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +2921 -2290
- package/dist/edition/verify-image-evidence.bundle.provenance.json +481 -415
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/store-submit.d.ts.map +1 -1
- package/dist/runtime/store-submit.js.map +1 -1
- package/edition/README.md +2 -3
- package/edition/assemble-producer-input.mjs +1 -1
- package/edition/biome-content.mjs +6 -0
- package/edition/build-platform-service-index.mjs +2 -0
- package/edition/build-runtime-image-source-index.mjs +21 -6
- package/edition/catalogue.mjs +5 -1
- package/edition/checkout-indexed-source-repositories.mjs +41 -6
- package/edition/dispatched-runtime-components.mjs +82 -0
- package/edition/edition-protocol.mjs +2 -2
- package/edition/external-source-walk.mjs +1 -1
- package/edition/image-evidence.mjs +1 -1
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/lib.mjs +119 -3
- package/edition/operation-grant-admission.mjs +70 -0
- package/edition/operation-requirements.mjs +128 -0
- package/edition/platform-lock-readability.mjs +2 -2
- package/edition/platform-service-class.mjs +42 -0
- package/edition/platform-service-ownership.mjs +78 -0
- package/edition/remedy-command.mjs +2 -2
- package/edition/resolve-customer-edition.mjs +4 -1
- package/edition/resolve-distribution.mjs +65 -15
- package/edition/resolve-images.mjs +65 -4
- package/edition/seal-image-digests.mjs +1 -1
- 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 (
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 }) });
|