@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.
- package/README.md +60 -389
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.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 +10030 -1491
- package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
- 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 +50 -271
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/assemble-producer-input.mjs +1 -1
- package/edition/authorization-base.mjs +18 -9
- package/edition/biome-content.mjs +6 -0
- package/edition/build-platform-service-index.mjs +2 -0
- package/edition/build-runtime-image-source-index.mjs +97 -9
- package/edition/catalogue.mjs +20 -13
- package/edition/checkout-indexed-source-repositories.mjs +41 -6
- package/edition/customer-definition.mjs +3 -20
- package/edition/develop-customer-edition.mjs +2 -2
- package/edition/develop-edition-resolve.mjs +10 -2
- package/edition/develop-edition.mjs +56 -62
- package/edition/develop-freeze.mjs +14 -3
- package/edition/dispatched-runtime-components.mjs +82 -0
- package/edition/dist-check.mjs +1 -0
- package/edition/distribution-evidence-set.mjs +32 -5
- package/edition/edition-channel.mjs +15 -1
- package/edition/edition-locks.mjs +16 -6
- package/edition/edition-protocol.mjs +30 -8
- package/edition/edition-workspace.mjs +8 -0
- package/edition/external-source-walk.mjs +1 -1
- package/edition/image-evidence.mjs +134 -26
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +192 -4
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/operation-grant-admission.mjs +70 -0
- package/edition/operation-requirements.mjs +128 -0
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- 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/produce-distribution-completeness-evidence.mjs +5 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
- package/edition/release-catalogue.mjs +10 -3
- package/edition/remedy-command.mjs +2 -2
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +33 -34
- package/edition/resolve-distribution.mjs +70 -16
- package/edition/resolve-edition.mjs +3 -2
- package/edition/resolve-images.mjs +65 -4
- package/edition/seal-image-digests.mjs +17 -5
- package/edition/trust-policy-arming-verb.mjs +2 -10
- package/edition/verify-image-evidence.mjs +1 -0
- package/examples/README.md +3 -2
- 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 (
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) };
|