@xemahq/xema 0.26.0 → 0.28.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 +70 -37
- package/dist/assets/distribution-assets.manifest.json +1 -2
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.js +1 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +7 -1
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/edition.d.ts +3 -0
- package/dist/commands/edition.d.ts.map +1 -0
- package/dist/commands/edition.js +46 -0
- package/dist/commands/edition.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/serve.js +1 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/up.js +1 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/env-defaults.d.ts.map +1 -1
- package/dist/dev/env-defaults.js +5 -2
- package/dist/dev/env-defaults.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts +3 -2
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +18 -8
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +54283 -0
- package/dist/edition/verify-image-evidence.bundle.provenance.json +2877 -0
- package/dist/runtime/config/schema.d.ts +1 -0
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/custody/local-custody.d.ts +12 -0
- package/dist/runtime/custody/local-custody.d.ts.map +1 -0
- package/dist/runtime/custody/local-custody.js +22 -0
- package/dist/runtime/custody/local-custody.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js +50 -0
- package/dist/runtime/custody/openbao-custody-bootstrap.js.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts +18 -0
- package/dist/runtime/custody/openbao-custody-store.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-custody-store.js +55 -0
- package/dist/runtime/custody/openbao-custody-store.js.map +1 -0
- package/dist/runtime/custody/openbao-http-client.d.ts +34 -0
- package/dist/runtime/custody/openbao-http-client.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-http-client.js +154 -0
- package/dist/runtime/custody/openbao-http-client.js.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts +22 -0
- package/dist/runtime/custody/openbao-local-constants.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-constants.js +48 -0
- package/dist/runtime/custody/openbao-local-constants.js.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts +16 -0
- package/dist/runtime/custody/openbao-local-runner.d.ts.map +1 -0
- package/dist/runtime/custody/openbao-local-runner.js +86 -0
- package/dist/runtime/custody/openbao-local-runner.js.map +1 -0
- package/dist/runtime/edition/run-entry.d.ts +9 -0
- package/dist/runtime/edition/run-entry.d.ts.map +1 -0
- package/dist/runtime/edition/run-entry.js +36 -0
- package/dist/runtime/edition/run-entry.js.map +1 -0
- package/dist/runtime/edition/verb-map.d.ts +60 -0
- package/dist/runtime/edition/verb-map.d.ts.map +1 -0
- package/dist/runtime/edition/verb-map.js +146 -0
- package/dist/runtime/edition/verb-map.js.map +1 -0
- package/dist/runtime/env-seeder.d.ts +2 -0
- package/dist/runtime/env-seeder.d.ts.map +1 -1
- package/dist/runtime/env-seeder.js +18 -3
- package/dist/runtime/env-seeder.js.map +1 -1
- package/dist/runtime/launch.d.ts +2 -1
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +31 -7
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/paths.d.ts +1 -2
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +0 -10
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/runner/chart-local-provenance.d.ts +20 -0
- package/dist/runtime/runner/chart-local-provenance.d.ts.map +1 -0
- package/dist/runtime/runner/chart-local-provenance.js +114 -0
- package/dist/runtime/runner/chart-local-provenance.js.map +1 -0
- package/dist/runtime/runner/chart-preflight.d.ts +8 -8
- package/dist/runtime/runner/chart-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/chart-preflight.js +30 -69
- package/dist/runtime/runner/chart-preflight.js.map +1 -1
- package/dist/runtime/runner/chart-pull.d.ts +14 -0
- package/dist/runtime/runner/chart-pull.d.ts.map +1 -0
- package/dist/runtime/runner/chart-pull.js +96 -0
- package/dist/runtime/runner/chart-pull.js.map +1 -0
- package/dist/runtime/runner/chart-resolver.d.ts +4 -12
- package/dist/runtime/runner/chart-resolver.d.ts.map +1 -1
- package/dist/runtime/runner/chart-resolver.js +16 -62
- package/dist/runtime/runner/chart-resolver.js.map +1 -1
- package/dist/runtime/runner/chart-set-lock.d.ts +6 -20
- package/dist/runtime/runner/chart-set-lock.d.ts.map +1 -1
- package/dist/runtime/runner/chart-set-lock.js +55 -99
- package/dist/runtime/runner/chart-set-lock.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +3 -1
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +9 -7
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +4 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +18 -16
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/sources/kinds/git.d.ts +1 -2
- package/dist/runtime/sources/kinds/git.d.ts.map +1 -1
- package/dist/runtime/sources/kinds/git.js +8 -5
- package/dist/runtime/sources/kinds/git.js.map +1 -1
- package/dist/runtime/sources/manifest-walk.d.ts +3 -1
- package/dist/runtime/sources/manifest-walk.d.ts.map +1 -1
- package/dist/runtime/sources/manifest-walk.js +8 -3
- package/dist/runtime/sources/manifest-walk.js.map +1 -1
- package/dist/runtime/sources/schema.d.ts +3 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +6 -1
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor.js +1 -0
- package/dist/runtime/supervisor.js.map +1 -1
- package/edition/README.md +313 -0
- package/edition/assemble-producer-input.mjs +1251 -0
- package/edition/authorization-base.mjs +347 -0
- package/edition/biome-content.mjs +268 -0
- package/edition/build-biome-index.mjs +175 -0
- package/edition/build-platform-service-index.mjs +288 -0
- package/edition/build-runtime-image-source-index.mjs +821 -0
- package/edition/built-verifier.mjs +55 -0
- package/edition/catalogue-edition.mjs +49 -0
- package/edition/catalogue-fetch.mjs +64 -0
- package/edition/catalogue.mjs +135 -0
- package/edition/chart-set-source-commit.mjs +92 -0
- package/edition/check-committed-image-evidence.mjs +383 -0
- package/edition/checkout-indexed-source-repositories.mjs +105 -0
- package/edition/comment-blanker.mjs +75 -0
- package/edition/connector-source-authority.mjs +60 -0
- package/edition/customer-definition.mjs +183 -0
- package/edition/customer-flags.mjs +7 -0
- package/edition/develop-customer-edition.mjs +99 -0
- package/edition/develop-edition-resolve.mjs +65 -0
- package/edition/develop-edition.mjs +274 -0
- package/edition/develop-freeze.mjs +173 -0
- package/edition/develop-image-coordinate.mjs +54 -0
- package/edition/develop-seal.mjs +77 -0
- package/edition/develop-snapshot-inputs.mjs +104 -0
- package/edition/dist-check.mjs +986 -0
- package/edition/distribution-evidence-set.mjs +987 -0
- package/edition/distribution-paths.mjs +191 -0
- package/edition/edition-artifact.mjs +284 -0
- package/edition/edition-biome-manifests.mjs +85 -0
- package/edition/edition-channel.mjs +6 -0
- package/edition/edition-chart-set.mjs +93 -0
- package/edition/edition-config.mjs +205 -0
- package/edition/edition-constants.mjs +57 -0
- package/edition/edition-contract.json +30 -0
- package/edition/edition-list.mjs +35 -0
- package/edition/edition-locks.mjs +69 -0
- package/edition/edition-protocol.mjs +228 -0
- package/edition/edition-workspace.mjs +187 -0
- package/edition/evidence-checkout.mjs +439 -0
- package/edition/evidence-grammars.mjs +16 -0
- package/edition/evidence-trust-pin.mjs +304 -0
- package/edition/evidence-trust-policy.mjs +294 -0
- package/edition/external-source-walk.mjs +463 -0
- package/edition/first-party-source-ids.mjs +59 -0
- package/edition/fleet-coordinate-finality.mjs +350 -0
- package/edition/git-source-auth.mjs +52 -0
- package/edition/image-delivery-entitlement-descriptor.mjs +106 -0
- package/edition/image-evidence-replay-state.mjs +242 -0
- package/edition/image-evidence.mjs +1233 -0
- package/edition/kernel-contract-authority.mjs +443 -0
- package/edition/lib.mjs +3021 -0
- package/edition/no-follow-file.mjs +94 -0
- package/edition/oci-evidence-verbs.mjs +392 -0
- package/edition/oci-image-source-evidence.mjs +239 -0
- package/edition/oras.mjs +648 -0
- package/edition/pin-source.mjs +227 -0
- package/edition/plan-image-evidence-refresh.mjs +408 -0
- package/edition/platform-lock-readability.mjs +892 -0
- package/edition/platform-service-class.mjs +18 -0
- package/edition/platform-service-source-authority.mjs +105 -0
- package/edition/positive-closure.mjs +162 -0
- package/edition/produce-distribution-completeness-evidence.mjs +311 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +294 -0
- package/edition/produce-image-source-evidence.mjs +765 -0
- package/edition/producer-evidence-bundle.mjs +157 -0
- package/edition/registry-credentials.mjs +193 -0
- package/edition/release-catalogue.mjs +39 -0
- package/edition/release-channel-guard.mjs +109 -0
- package/edition/release-edition-artifact.mjs +157 -0
- package/edition/remedy-command.mjs +89 -0
- package/edition/repin-evidence-trust.mjs +101 -0
- package/edition/required-resource-provider.mjs +58 -0
- package/edition/resolve-chart-set.mjs +415 -0
- package/edition/resolve-customer-edition.mjs +288 -0
- package/edition/resolve-distribution.mjs +2318 -0
- package/edition/resolve-edition.mjs +186 -0
- package/edition/resolve-images.mjs +1606 -0
- package/edition/seal-image-digests.mjs +855 -0
- package/edition/sealed-evidence-accept.mjs +82 -0
- package/edition/source-authority.mjs +725 -0
- package/edition/source-closure.mjs +116 -0
- package/edition/strict-json.mjs +121 -0
- package/edition/string-order.mjs +5 -0
- package/edition/trust-policy-arming.mjs +120 -0
- package/edition/trusted-executable.mjs +166 -0
- package/edition/verifier-binding.mjs +45 -0
- package/edition/verifier-files.mjs +8 -0
- package/edition/verifier-kernel-schemas.mjs +22 -0
- package/edition/verifier-provenance.mjs +64 -0
- package/edition/verify-image-evidence.mjs +458 -0
- package/edition/verify-image-platforms.mjs +495 -0
- package/edition/why.mjs +56 -0
- package/edition/workspace-sections.mjs +195 -0
- package/examples/06-biome-sources.yaml +1 -0
- package/package.json +14 -6
- package/dist/commands/dist.d.ts +0 -16
- package/dist/commands/dist.d.ts.map +0 -1
- package/dist/commands/dist.js +0 -182
- package/dist/commands/dist.js.map +0 -1
- package/dist/runtime/distribution/available-biome.d.ts +0 -10
- package/dist/runtime/distribution/available-biome.d.ts.map +0 -1
- package/dist/runtime/distribution/available-biome.js +0 -156
- package/dist/runtime/distribution/available-biome.js.map +0 -1
- package/dist/runtime/distribution/compose.d.ts +0 -23
- package/dist/runtime/distribution/compose.d.ts.map +0 -1
- package/dist/runtime/distribution/compose.js +0 -48
- package/dist/runtime/distribution/compose.js.map +0 -1
- package/dist/runtime/runner/chart-trust.d.ts +0 -57
- package/dist/runtime/runner/chart-trust.d.ts.map +0 -1
- package/dist/runtime/runner/chart-trust.js +0 -270
- package/dist/runtime/runner/chart-trust.js.map +0 -1
package/edition/lib.mjs
ADDED
|
@@ -0,0 +1,3021 @@
|
|
|
1
|
+
// Shared helpers for the Xema distribution packaging tooling.
|
|
2
|
+
//
|
|
3
|
+
// These are pure Node ESM utilities (mjs tooling — Date.now()/clocks are fine
|
|
4
|
+
// here, unlike kernel contracts). They centralize the few things the three
|
|
5
|
+
// distribution scripts share: locating the repo root, resolving the BUILT
|
|
6
|
+
// kernel contract schemas, the closed BiomeTier order, and the on-disk biome
|
|
7
|
+
// walk. Keeping them here avoids duplicating the manifest-reading + tier-order
|
|
8
|
+
// logic across build/resolve/check. Trusted index generation reads immutable
|
|
9
|
+
// Git objects rather than mutable working-tree files.
|
|
10
|
+
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { isIP } from 'node:net';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { readBiomeAuthorizationModuleAt } from './authorization-base.mjs';
|
|
18
|
+
import {
|
|
19
|
+
isAuthoritativeProductionBiomeSource,
|
|
20
|
+
} from './connector-source-authority.mjs';
|
|
21
|
+
import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
22
|
+
import {
|
|
23
|
+
dataRoot,
|
|
24
|
+
kernelDistPath,
|
|
25
|
+
kernelPackageDir,
|
|
26
|
+
kernelRepoDir,
|
|
27
|
+
sourceHashPackageDir,
|
|
28
|
+
sourcePath,
|
|
29
|
+
} from './edition-workspace.mjs';
|
|
30
|
+
import { SHA256_HEX_PATTERN } from './evidence-grammars.mjs';
|
|
31
|
+
import {
|
|
32
|
+
createSourceAuthority,
|
|
33
|
+
DistributionSourceMode,
|
|
34
|
+
PUBLISHED_SOURCE_REF,
|
|
35
|
+
publishedSourceCommit,
|
|
36
|
+
} from './source-authority.mjs';
|
|
37
|
+
import { parseStrictJson, readStrictJsonFile } from './strict-json.mjs';
|
|
38
|
+
import { compareStrings } from './string-order.mjs';
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
compareStrings,
|
|
42
|
+
createSourceAuthority,
|
|
43
|
+
DistributionSourceMode,
|
|
44
|
+
PUBLISHED_SOURCE_REF,
|
|
45
|
+
publishedSourceCommit,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Closed BiomeTier order (low→high), READ FROM THE KERNEL.
|
|
51
|
+
*
|
|
52
|
+
* The VOCABULARY and the ORDER are both `@xemahq/contracts/biome`'s:
|
|
53
|
+
* `BIOME_TIER_ORDER` in `src/biome/lib/biome-tier-rank.ts`, itself derived
|
|
54
|
+
* from `BIOME_TIER_RANK` — a map exhaustively keyed on the `BiomeTier` enum, so
|
|
55
|
+
* a fifth tier is a compile error in the kernel rather than a value that reads
|
|
56
|
+
* back `undefined` here and sorts silently to one end.
|
|
57
|
+
*
|
|
58
|
+
* The package NAME above is `@xemahq/contracts`, not the retired
|
|
59
|
+
* `@xemahq/kernel-contracts`; the repository directory is still
|
|
60
|
+
* `repos/xema-kernel-contracts`, which is why the dist path below keeps it.
|
|
61
|
+
* Re-measured at that repository's `origin/main` (1e2f7d7, 2026-09-15):
|
|
62
|
+
* `packages/kernel/kernel-contracts/package.json` declares `@xemahq/contracts`,
|
|
63
|
+
* `BIOME_TIER_ORDER` is exported from `src/biome/lib/biome-tier-rank.ts`, and
|
|
64
|
+
* `src/biome/index.ts` re-exports it.
|
|
65
|
+
*
|
|
66
|
+
* ── This replaced a literal, and the comment above it was FALSE ──
|
|
67
|
+
*
|
|
68
|
+
* That comment said "the kernel exports no order constant — there is no
|
|
69
|
+
* `BIOME_TIER_ORDER` in `@xemahq/kernel-contracts` (verified 2026-08-31)", and
|
|
70
|
+
* justified the private copy with "so the tooling does not depend on the kernel
|
|
71
|
+
* package being workspace-linked at the repo root (it is not)". Both halves
|
|
72
|
+
* failed, and each failed differently:
|
|
73
|
+
*
|
|
74
|
+
* • The FIRST half was true when written and is not now. The kernel ships
|
|
75
|
+
* `BIOME_TIER_RANK`, `BIOME_TIER_ORDER`, `biomeTierRank` and
|
|
76
|
+
* `biomeTierWithin`, re-exported by the `biome` barrel — and that file's own
|
|
77
|
+
* header names `['infra', ...BIOME_TIER_ORDER, 'edge']` in
|
|
78
|
+
* `resolve-distribution.mjs` as the hand-rolled copy it exists to delete.
|
|
79
|
+
* • The SECOND half was never a reason. It is true that a BARE
|
|
80
|
+
* `@xemahq/kernel-contracts` specifier does not resolve here — this repo's
|
|
81
|
+
* `package.json` declares one devDependency and it is `esbuild` — but this
|
|
82
|
+
* module has never used one. It reaches the kernel by the deterministic dist
|
|
83
|
+
* PATH below (`loadKernelBiomeContracts`), and `readBiomeManifest`, the very
|
|
84
|
+
* function that validated a tier against the deleted literal, already called
|
|
85
|
+
* that loader four lines later for `isBiomeStoreListed`. The kernel was a
|
|
86
|
+
* hard precondition of this exact code path the whole time.
|
|
87
|
+
*
|
|
88
|
+
* A justification that names a real constraint which does not apply is the
|
|
89
|
+
* worst shape of the three, because the constraint checks out: a reader
|
|
90
|
+
* verifying "the bare specifier does not resolve" confirms the sentence and
|
|
91
|
+
* stops. See `.claude/rules/writing-boundary-checks.md`, "A comment that
|
|
92
|
+
* JUSTIFIES a limitation is the last place anyone re-derives it".
|
|
93
|
+
*
|
|
94
|
+
* ── Why a function and not a const ──
|
|
95
|
+
*
|
|
96
|
+
* `loadKernelBiomeContracts` throws when the kernel dist is unbuilt. Reading it
|
|
97
|
+
* at module scope would make merely IMPORTING this module require a built
|
|
98
|
+
* kernel, for every consumer including those that never rank a tier. Lazy and
|
|
99
|
+
* memoized keeps the precondition where it already was.
|
|
100
|
+
*
|
|
101
|
+
* A remaining duplicate, deliberately not taken here: `PLATFORM_SERVICE_TIER_RANK`
|
|
102
|
+
* (kernel `src/distribution`) is `['infra', ...biome ladder, 'edge']` already
|
|
103
|
+
* derived, which is what `resolve-distribution.mjs` still builds by hand. That
|
|
104
|
+
* subpath is loaded through a DIFFERENT, async loader
|
|
105
|
+
* (`loadKernelDistributionSchemas`) and is a rank MAP rather than an array, so
|
|
106
|
+
* adopting it is its own change with its own call-site shape.
|
|
107
|
+
*/
|
|
108
|
+
export function biomeTierOrder() {
|
|
109
|
+
return loadKernelBiomeContracts().BIOME_TIER_ORDER;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Is `tier` a valid BiomeTier? (closed set — callers fail fast on false). */
|
|
113
|
+
export function isBiomeTier(tier) {
|
|
114
|
+
return biomeTierOrder().includes(tier);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Filename of the committed per-biome generated index, beside every
|
|
119
|
+
* `xema-biome.json`. Value-identical to `BIOME_GENERATED_INDEX_FILENAME` in
|
|
120
|
+
* `@xemahq/contracts/biome`; asserted against the BUILT kernel export by
|
|
121
|
+
* `loadKernelBiomeContracts`, so the two cannot drift silently.
|
|
122
|
+
*/
|
|
123
|
+
export const BIOME_GENERATED_INDEX_FILENAME = 'xema-biome.index.json';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Closed workload set for the v4 `xema.ships.apis[]` manifest shape this
|
|
127
|
+
* tooling reads.
|
|
128
|
+
*
|
|
129
|
+
* NO kernel enum spells it: there is no `BiomeWorkloadKind` and no
|
|
130
|
+
* `biome-workload-kind.ts` anywhere in `@xemahq/contracts` (verified
|
|
131
|
+
* against that repo's `origin/main`, 2026-08-31), so this literal is the only
|
|
132
|
+
* declaration of the set — it mirrors nothing. The v5 successor of the same
|
|
133
|
+
* distinction is `BiomeComponentScheduler` (`platform | runtime`) in
|
|
134
|
+
* `@xemahq/contracts/biome`
|
|
135
|
+
* (`src/biome/lib/biome-component-vocabulary.ts`), whose own docblock names
|
|
136
|
+
* `ships.apis[].workload: "runtime-component"` as the v4 spelling it carries
|
|
137
|
+
* forward. When this tooling moves to `components[]`, map onto that rather
|
|
138
|
+
* than re-deriving the distinction here. An entry of `xema.ships.apis[]` that
|
|
139
|
+
* omits `workload` resolves to `DEFAULT_BIOME_WORKLOAD_KIND`.
|
|
140
|
+
*
|
|
141
|
+
* - `service` — a standing cluster workload the deploy plane rosters.
|
|
142
|
+
* - `runtime-component` — an image built/published/signed/mirrored like any
|
|
143
|
+
* other but NEVER independently scheduled; another workload owns its
|
|
144
|
+
* lifecycle. No chart, no release, no roster entry.
|
|
145
|
+
*/
|
|
146
|
+
export const BIOME_WORKLOAD_KINDS = new Set(['service', 'runtime-component']);
|
|
147
|
+
|
|
148
|
+
/** The kind applied when a `ships.apis[]` entry omits `workload`. */
|
|
149
|
+
export const DEFAULT_BIOME_WORKLOAD_KIND = 'service';
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The BUILT `@xemahq/distribution-source-hash` package (CJS dist), loaded from
|
|
153
|
+
* the directory the entry states with `--source-hash`.
|
|
154
|
+
*
|
|
155
|
+
* That package is the SINGLE owner of `xema-source-tree-sha256-v2` — the same
|
|
156
|
+
* implementation every producing repository's CI uses to derive the
|
|
157
|
+
* `<contentHash>` release tag. This tooling must never carry a second copy: a
|
|
158
|
+
* divergence would put a lock and its images at coordinates that silently
|
|
159
|
+
* disagree. It is NOT an npm dependency of the CLI: the evidence verifier never
|
|
160
|
+
* hashes a source tree, so its bundle must not reach this package; only the
|
|
161
|
+
* entries that resolve, build, index or seal state the root.
|
|
162
|
+
*/
|
|
163
|
+
const requireFromLib = createRequire(import.meta.url);
|
|
164
|
+
|
|
165
|
+
const loadedSourceHash = new Map();
|
|
166
|
+
|
|
167
|
+
function distributionSourceHash() {
|
|
168
|
+
const directory = sourceHashPackageDir();
|
|
169
|
+
const known = loadedSourceHash.get(directory);
|
|
170
|
+
if (known) {return known;}
|
|
171
|
+
let mod;
|
|
172
|
+
try {
|
|
173
|
+
mod = createRequire(join(directory, 'package.json'))(directory);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Cannot load the built distribution-source-hash package from ${directory}. ` +
|
|
177
|
+
`Build it first (\`pnpm install --frozen-lockfile && pnpm run build\` in its workspace). ` +
|
|
178
|
+
`Underlying error: ${err.message}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
assertSourceHashMatchesKernel(mod);
|
|
182
|
+
loadedSourceHash.set(directory, mod);
|
|
183
|
+
return mod;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The one source-TREE hash algorithm identifier this tooling accepts — read
|
|
188
|
+
* from the implementation that actually derives the hash, never a local
|
|
189
|
+
* literal. `loadKernelDistributionSchemas` asserts it is value-identical to the
|
|
190
|
+
* kernel's `ImageSourceHashAlgorithm.MetadataV2`.
|
|
191
|
+
*
|
|
192
|
+
* This hashes ONE directory. It is a COMPONENT of an image coordinate, never an
|
|
193
|
+
* image coordinate on its own.
|
|
194
|
+
*/
|
|
195
|
+
function metadataSourceHashAlgorithm(sourceHash) {
|
|
196
|
+
return requireAlgorithmMember(
|
|
197
|
+
sourceHash.SourceTreeHashAlgorithm,
|
|
198
|
+
'SourceTreeHashAlgorithm',
|
|
199
|
+
'MetadataV2',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Read one member off a built enum, refusing `undefined`.
|
|
205
|
+
*
|
|
206
|
+
* A missing member on a JS enum object is an ordinary property read: it yields
|
|
207
|
+
* `undefined` and nothing complains at the read site. That is not hypothetical
|
|
208
|
+
* here — renaming `ImageInputV1` to `ImageInputV2` in the source-hash package
|
|
209
|
+
* turned every reader of the old name into a silent `undefined`, which reaches
|
|
210
|
+
* a lock's `sourceHashAlgorithm` and a producer statement before anything
|
|
211
|
+
* notices. Failing AT THE READ names the stale artifact instead.
|
|
212
|
+
*/
|
|
213
|
+
function requireAlgorithmMember(enumObject, enumName, member) {
|
|
214
|
+
const value = enumObject?.[member];
|
|
215
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Built @xemahq/distribution-source-hash does not declare ` +
|
|
218
|
+
`${enumName}.${member}. The installed package is `+
|
|
219
|
+
`stale or the member was renamed; raise the @xemahq/distribution-source-hash range and, if the `+
|
|
220
|
+
`rename was deliberate, move every reader in the same change.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The one IMAGE-BUILD-INPUT closure algorithm this tooling MINTS, and the only
|
|
228
|
+
* value admissible as an `ImageLock.sourceHashAlgorithm`. A subject's
|
|
229
|
+
* `contentHash` under this algorithm covers the whole declared build-input
|
|
230
|
+
* closure of its image, not just its own subtree.
|
|
231
|
+
*
|
|
232
|
+
* Read from the implementation that derives the hash, never a local literal,
|
|
233
|
+
* so "the algorithm this tooling accepts" and "the algorithm CI can compute"
|
|
234
|
+
* cannot drift apart. That implementation's `ImageBuildInputHashAlgorithm`
|
|
235
|
+
* declares `ImageInputV2` and `ImageInputV3`; this tooling mints only
|
|
236
|
+
* `ImageInputV2`. `@xemahq/contracts` 0.70.0 declares `ImageInputV3` (kernel-
|
|
237
|
+
* contracts 259c8f5e), but every producer's release lane (and the develop lane,
|
|
238
|
+
* contract C1) computes V2 keys, so a V3 lock would name coordinates no producer
|
|
239
|
+
* pushes. V3 adoption is increment 2 and moves the producers and this function
|
|
240
|
+
* together.
|
|
241
|
+
*
|
|
242
|
+
* The kernel's `ImageSourceHashAlgorithm` retains `ImageInputV1` because signed
|
|
243
|
+
* in-toto statements and already-published locks carry that string and are
|
|
244
|
+
* immutable. This function is deliberately not a set: a lock the current
|
|
245
|
+
* producer cannot reproduce is not deliverable, so a v1 lock must be
|
|
246
|
+
* REGENERATED, never silently accepted.
|
|
247
|
+
*/
|
|
248
|
+
export function imageInputSourceHashAlgorithm() {
|
|
249
|
+
return imageInputSourceHashAlgorithmOf(distributionSourceHash());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function imageInputSourceHashAlgorithmOf(sourceHash) {
|
|
253
|
+
return requireAlgorithmMember(
|
|
254
|
+
sourceHash.ImageBuildInputHashAlgorithm,
|
|
255
|
+
'ImageBuildInputHashAlgorithm',
|
|
256
|
+
'ImageInputV2',
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Hold the built source-hash package value-identical to the kernel's wire names.
|
|
262
|
+
* Runs once per loaded package, so no hash is ever derived from a package that
|
|
263
|
+
* names its algorithms differently from the kernel that parses the lock.
|
|
264
|
+
*/
|
|
265
|
+
function assertSourceHashMatchesKernel(sourceHash) {
|
|
266
|
+
const { ImageSourceHashAlgorithm } = requireFromLib(kernelDistributionDist());
|
|
267
|
+
if (typeof ImageSourceHashAlgorithm !== 'object' || ImageSourceHashAlgorithm === null) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Built kernel distribution module at ${kernelDistributionDist()} does not export ImageSourceHashAlgorithm — the dist is stale; rebuild kernel-contracts.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
// The kernel owns the WIRE name of the source-hash algorithm; the
|
|
273
|
+
// source-hash package owns its IMPLEMENTATION and declares the identifier
|
|
274
|
+
// itself so it can ship dependency-free to every producing repo's CI. This
|
|
275
|
+
// is the one place both built artifacts are in hand, so it is where the two
|
|
276
|
+
// are held value-identical — a drift here would put a lock's `contentHash`
|
|
277
|
+
// and the tag CI pushes at coordinates that silently disagree.
|
|
278
|
+
//
|
|
279
|
+
// BOTH members are held identical. `MetadataV2` names the component tree
|
|
280
|
+
// digest; `ImageInputV2` names the full image build-input closure and is what
|
|
281
|
+
// a lock declares. A drift in either would put a lock's `contentHash` and the
|
|
282
|
+
// tag CI pushes at coordinates that silently disagree.
|
|
283
|
+
//
|
|
284
|
+
// The kernel enum is a SUPERSET: it keeps `ImageInputV1` so an already-signed
|
|
285
|
+
// statement stays readable, while the source-hash package declares only what
|
|
286
|
+
// it can still COMPUTE. So the assertion pairs each implemented member with
|
|
287
|
+
// the kernel member of the same name — it is not a set comparison, and adding
|
|
288
|
+
// a retired wire name to the kernel must not make this pass vacuously.
|
|
289
|
+
for (const [implemented, declared, label] of [
|
|
290
|
+
[
|
|
291
|
+
metadataSourceHashAlgorithm(sourceHash),
|
|
292
|
+
ImageSourceHashAlgorithm.MetadataV2,
|
|
293
|
+
'MetadataV2',
|
|
294
|
+
],
|
|
295
|
+
[
|
|
296
|
+
imageInputSourceHashAlgorithmOf(sourceHash),
|
|
297
|
+
ImageSourceHashAlgorithm.ImageInputV2,
|
|
298
|
+
'ImageInputV2',
|
|
299
|
+
],
|
|
300
|
+
]) {
|
|
301
|
+
if (implemented !== declared) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Source-hash algorithm drift (${label}): @xemahq/distribution-source-hash ` +
|
|
304
|
+
`implements "${String(implemented)}" but @xemahq/contracts declares ` +
|
|
305
|
+
`"${String(declared)}". Both must name the same algorithm; rebuild both ` +
|
|
306
|
+
`packages, and if they genuinely diverged, fix the producer side before ` +
|
|
307
|
+
`any lock is written.`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Path to the BUILT kernel-contracts BIOME module (CJS dist). */
|
|
314
|
+
const kernelBiomeDist = () => kernelDistPath('biome', 'index.js');
|
|
315
|
+
|
|
316
|
+
let kernelBiomeContracts;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The kernel's biome contracts, loaded SYNCHRONOUSLY (`createRequire`, like the
|
|
320
|
+
* source-hash module above) because the index builder is synchronous.
|
|
321
|
+
*
|
|
322
|
+
* It lives here rather than in each consumer because `isBiomeStoreListed` is a
|
|
323
|
+
* predicate with derived refusals, and a second reading of it is not a style
|
|
324
|
+
* problem — it is a wrong answer. `manifestToIndexEntry` used to resolve
|
|
325
|
+
* `storeListed` as `xema.storeListed ?? true`, which skips BOTH refusals the
|
|
326
|
+
* kernel applies: an `operator`-audience biome and a `kernel`-tier biome are
|
|
327
|
+
* never listed, whatever the flag says. So the index advertised every kernel
|
|
328
|
+
* biome as store-listed while the catalog projector — which calls the kernel
|
|
329
|
+
* function — refused them, and the two disagreed about the same bytes. The
|
|
330
|
+
* kernel's own docstring says it in as many words: resolve via
|
|
331
|
+
* `isBiomeStoreListed`, never read the field raw.
|
|
332
|
+
*/
|
|
333
|
+
export function loadKernelBiomeContracts() {
|
|
334
|
+
assertKernelEditionProtocol(kernelPackageDir());
|
|
335
|
+
if (kernelBiomeContracts) {return kernelBiomeContracts;}
|
|
336
|
+
let mod;
|
|
337
|
+
try {
|
|
338
|
+
mod = requireFromLib(kernelBiomeDist());
|
|
339
|
+
} catch (err) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Cannot load built kernel biome contracts from ${kernelBiomeDist()}. ` +
|
|
342
|
+
`Build it first: run \`pnpm install --frozen-lockfile && pnpm run build\` ` +
|
|
343
|
+
`in the kernel checkout (${kernelRepoDir()}). That names no package deliberately — ` +
|
|
344
|
+
`a remedy naming one goes stale on a rename, silently, because ` +
|
|
345
|
+
`\`pnpm --filter\` exits 0 when nothing matches. ` +
|
|
346
|
+
`Underlying error: ${err.message}`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (
|
|
350
|
+
typeof mod.parseBiomeManifest !== 'function' ||
|
|
351
|
+
typeof mod.isBiomeStoreListed !== 'function' ||
|
|
352
|
+
typeof mod.BiomeTrustTier !== 'object' ||
|
|
353
|
+
typeof mod.BiomeOrigin !== 'object' ||
|
|
354
|
+
typeof mod.requiredBiomeDependencyIds !== 'function' ||
|
|
355
|
+
typeof mod.foldInstallationClass !== 'function' ||
|
|
356
|
+
typeof mod.BiomeComponentInstallationClass !== 'object' ||
|
|
357
|
+
// The GRAMMAR a service component's `protocol.databaseExtensions` entries
|
|
358
|
+
// are validated against. Read from the kernel, never restated here.
|
|
359
|
+
//
|
|
360
|
+
// A PREDICATE, not a set, and that is the whole point: a PostgreSQL
|
|
361
|
+
// extension name is an OPEN vocabulary owned by whoever runs the database,
|
|
362
|
+
// so the kernel declares `^[a-z_][a-z0-9_-]*$` (<=63) and ships
|
|
363
|
+
// `isPostgresExtensionName` rather than an enum. This guard asked for a
|
|
364
|
+
// closed `PostgresExtension` object the kernel has never exported, and its
|
|
365
|
+
// error told the reader to rebuild a dist that was already current --- a
|
|
366
|
+
// false remedy for a design disagreement. Whether a name may actually be
|
|
367
|
+
// INSTALLED is a deployment question (`infra.yaml#postgres.allowedExtensions`),
|
|
368
|
+
// deliberately not a contract one.
|
|
369
|
+
typeof mod.isPostgresExtensionName !== 'function' ||
|
|
370
|
+
typeof mod.BiomeBundleGeneratedIndexSchema?.parse !== 'function' ||
|
|
371
|
+
// The ONE shape a biome's `xema.bundleSource` is validated against before
|
|
372
|
+
// it is projected — onto a host module, or onto the biome itself. Read
|
|
373
|
+
// from the kernel so the index can never carry a source the lock schema
|
|
374
|
+
// (which reuses this exact schema) would then refuse.
|
|
375
|
+
typeof mod.BiomeBundleSourceSpecSchema?.safeParse !== 'function' ||
|
|
376
|
+
// A component's process facts (`@xemahq/contracts` >= 0.30.0): the
|
|
377
|
+
// scaling vocabulary and the intrinsic-facts block, projected into
|
|
378
|
+
// `serviceProcessFacts` by `componentProcessFacts`.
|
|
379
|
+
typeof mod.BiomeScalingMode !== 'object' ||
|
|
380
|
+
typeof mod.BiomeComponentProcessFactsSchema?.safeParse !== 'function' ||
|
|
381
|
+
// Worker protocol v2 (`@xemahq/contracts` >= 0.52.0): the ONE schema a
|
|
382
|
+
// worker's `protocol.taskQueues` / `protocol.nexus` are graded against
|
|
383
|
+
// before `componentNexusDeclarations` projects them into the index.
|
|
384
|
+
typeof mod.BiomeWorkerProtocolSchema?.safeParse !== 'function' ||
|
|
385
|
+
// The ONE schema a service's `protocol.workflowClientOf` entries are graded
|
|
386
|
+
// against (`@xemahq/contracts` >= 0.60.0), before `componentWorkflowClients`
|
|
387
|
+
// projects them into the index.
|
|
388
|
+
typeof mod.BiomeWorkflowClientOfSchema?.safeParse !== 'function' ||
|
|
389
|
+
// Asserted here, not read hopefully: `biomeTierOrder()` above returns this
|
|
390
|
+
// array straight to `.includes()` / `.indexOf()`, and an absent export
|
|
391
|
+
// would surface as a TypeError at some sort comparator rather than as the
|
|
392
|
+
// actionable "rebuild kernel-contracts" below.
|
|
393
|
+
!Array.isArray(mod.BIOME_TIER_ORDER) ||
|
|
394
|
+
mod.BIOME_TIER_ORDER.length === 0
|
|
395
|
+
) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Built kernel biome module at ${kernelBiomeDist()} does not export ` +
|
|
398
|
+
`parseBiomeManifest/isBiomeStoreListed/BiomeTrustTier/BiomeOrigin/` +
|
|
399
|
+
`requiredBiomeDependencyIds/foldInstallationClass/` +
|
|
400
|
+
`BiomeComponentInstallationClass/isPostgresExtensionName/` +
|
|
401
|
+
`BiomeBundleGeneratedIndexSchema/BiomeBundleSourceSpecSchema/BiomeScalingMode/` +
|
|
402
|
+
`BiomeComponentProcessFactsSchema/BiomeWorkerProtocolSchema/BiomeWorkflowClientOfSchema/BIOME_TIER_ORDER — ` +
|
|
403
|
+
`the dist is stale (isPostgresExtensionName needs @xemahq/contracts >= 0.12.0, ` +
|
|
404
|
+
`the process facts >= 0.30.0, worker protocol v2 >= 0.52.0, workflowClientOf >= 0.60.0); rebuild ` +
|
|
405
|
+
`kernel-contracts at a commit that carries it.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (mod.BIOME_GENERATED_INDEX_FILENAME !== BIOME_GENERATED_INDEX_FILENAME) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Kernel BIOME_GENERATED_INDEX_FILENAME is "${String(mod.BIOME_GENERATED_INDEX_FILENAME)}" ` +
|
|
411
|
+
`but this plane reads "${BIOME_GENERATED_INDEX_FILENAME}" — the literal drifted from the kernel.`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
kernelBiomeContracts = mod;
|
|
415
|
+
return kernelBiomeContracts;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Path to the BUILT kernel-contracts RESOURCE-REQUIREMENT module (CJS dist). */
|
|
419
|
+
const kernelResourceRequirementDist = () => kernelDistPath('resource-requirement', 'index.js');
|
|
420
|
+
|
|
421
|
+
let kernelResourceRequirementContracts;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The kernel's resource-requirement grammar, loaded SYNCHRONOUSLY like
|
|
425
|
+
* {@link loadKernelBiomeContracts}. A component's `protocol.resources` and
|
|
426
|
+
* `protocol.providesResources` are validated by the kernel's own schemas —
|
|
427
|
+
* never a restated grammar — so the index can never carry a declaration the
|
|
428
|
+
* lock schema would then refuse.
|
|
429
|
+
*/
|
|
430
|
+
export function loadKernelResourceRequirementContracts() {
|
|
431
|
+
assertKernelEditionProtocol(kernelPackageDir());
|
|
432
|
+
if (kernelResourceRequirementContracts) {
|
|
433
|
+
return kernelResourceRequirementContracts;
|
|
434
|
+
}
|
|
435
|
+
let mod;
|
|
436
|
+
try {
|
|
437
|
+
mod = requireFromLib(kernelResourceRequirementDist());
|
|
438
|
+
} catch (err) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`Cannot load built kernel resource-requirement contracts from ` +
|
|
441
|
+
`${kernelResourceRequirementDist()}. Build kernel-contracts at a commit ` +
|
|
442
|
+
`that carries the resource-requirement subpath: run ` +
|
|
443
|
+
`\`pnpm install --frozen-lockfile && pnpm run build\` in ` +
|
|
444
|
+
`the kernel checkout (${kernelRepoDir()}). Underlying error: ${err.message}`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
typeof mod.ResourceRequirementsSchema?.safeParse !== 'function' ||
|
|
449
|
+
typeof mod.ResourceProvisionsSchema?.safeParse !== 'function'
|
|
450
|
+
) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`Built kernel resource-requirement module at ${kernelResourceRequirementDist()} ` +
|
|
453
|
+
`does not export ResourceRequirementsSchema/ResourceProvisionsSchema — ` +
|
|
454
|
+
`the dist is stale; rebuild kernel-contracts.`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
kernelResourceRequirementContracts = mod;
|
|
458
|
+
return kernelResourceRequirementContracts;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Validate one component's `protocol.resources` / `protocol.providesResources`
|
|
463
|
+
* with the KERNEL's schemas, naming the manifest and the component on failure.
|
|
464
|
+
* Returns `[]` for an absent field — absent and `[]` make the same claim.
|
|
465
|
+
*/
|
|
466
|
+
function parseComponentResourceDeclaration(schemaName, value, where) {
|
|
467
|
+
if (value === undefined) {return [];}
|
|
468
|
+
const schema = loadKernelResourceRequirementContracts()[schemaName];
|
|
469
|
+
const parsed = schema.safeParse(value);
|
|
470
|
+
if (!parsed.success) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`${where} is not a valid resource declaration: ` +
|
|
473
|
+
parsed.error.issues
|
|
474
|
+
.map((issue) => `[${issue.path.join('.')}] ${issue.message}`)
|
|
475
|
+
.join('; '),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return parsed.data;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Project one image component's Nexus declarations (worker protocol v2) into
|
|
483
|
+
* the two index rows the edition resolver binds from. The protocol is graded
|
|
484
|
+
* by the KERNEL's `BiomeWorkerProtocolSchema` first, so every v2 rule (a
|
|
485
|
+
* provided service names one of the worker's own `nexus` queues, nothing is
|
|
486
|
+
* provided and required by one worker, no duplicates) holds before anything
|
|
487
|
+
* is projected. Returns empty arrays for a component that declares no `nexus`
|
|
488
|
+
* — absent and `[]` make the same claim — so a biome that declares none adds
|
|
489
|
+
* no index key and `biome-index.json` stays byte-stable.
|
|
490
|
+
*
|
|
491
|
+
* Only a `worker` may declare one: the v2 shape is the worker protocol's, and
|
|
492
|
+
* a `nexus` block on any other kind is refused rather than dropped, because a
|
|
493
|
+
* dropped requirement reaches no lock and fails nothing.
|
|
494
|
+
*/
|
|
495
|
+
/** The protocol kinds a `worker` component may declare (closed). */
|
|
496
|
+
const WorkerProtocolKind = Object.freeze({ Worker: 'worker', None: 'none' });
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* The task queues a protocol `worker/v2` component polls, sorted by name (a bare
|
|
500
|
+
* short name, or `{ name, placement }` when the queue is placed),
|
|
501
|
+
* or null for anything else — a runtime-launched worker (protocol `none`) polls
|
|
502
|
+
* nothing, and a service carries no worker protocol. Validated by the kernel's own worker-protocol
|
|
503
|
+
* schema, naming the manifest. The deploy plane runs exactly these components
|
|
504
|
+
* under a Worker Controller (durable-execution plan W3.4 / U14), so a v2 worker
|
|
505
|
+
* is recognised by what it declares, never by a list of chart names.
|
|
506
|
+
*/
|
|
507
|
+
function componentWorkerTaskQueues(component, protocol, where) {
|
|
508
|
+
if (component.kind !== 'worker') {return null;}
|
|
509
|
+
// A CLOSED enum: a worker protocol kind this resolver does not know would
|
|
510
|
+
// otherwise silently contribute no queues, and a missing queue reaches no lock.
|
|
511
|
+
if (!Object.values(WorkerProtocolKind).includes(protocol.kind)) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
`${where} declares worker protocol kind ${JSON.stringify(protocol.kind)}; ` +
|
|
514
|
+
`expected ${Object.values(WorkerProtocolKind).join(' | ')}`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
if (protocol.kind === WorkerProtocolKind.None) {return null;}
|
|
518
|
+
const parsed = loadKernelBiomeContracts().BiomeWorkerProtocolSchema.safeParse(protocol);
|
|
519
|
+
if (!parsed.success) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`${where} is not a valid worker protocol: ` +
|
|
522
|
+
parsed.error.issues.map((issue) => `[${issue.path.join('.')}] ${issue.message}`).join('; '),
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
// A queue with a `placement` keeps it: the lock carries `{ name, placement }`
|
|
526
|
+
// for it and the bare short name for every other queue.
|
|
527
|
+
return parsed.data.taskQueues
|
|
528
|
+
.map((queue) => lockedTaskQueueRef(queue.name, queue.placement))
|
|
529
|
+
.sort(compareTaskQueueRefs);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** The lock's `WorkerTaskQueueRef`: a bare short name, or `{ name, placement }` when the queue is placed. */
|
|
533
|
+
function lockedTaskQueueRef(name, placement) {
|
|
534
|
+
return placement === undefined ? name : { name, placement };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function taskQueueRefName(ref) {
|
|
538
|
+
return typeof ref === 'string' ? ref : ref.name;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function compareTaskQueueRefs(a, b) {
|
|
542
|
+
return compareStrings(taskQueueRefName(a), taskQueueRefName(b));
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* The `{ biome?, component, taskQueue }[]` a service's `protocol.workflowClientOf`
|
|
547
|
+
* declares (`biome` only when the entry names another biome's worker, passed
|
|
548
|
+
* through from the kernel schema's parse), sorted by biome (absent first), then
|
|
549
|
+
* component, then queue name (`taskQueue` is a bare short
|
|
550
|
+
* name, or `{ name, placement }` when the queue is placed), or `null` when it declares none (absent and `[]`
|
|
551
|
+
* make the same claim). Each entry is graded by the kernel's own schema; that
|
|
552
|
+
* the target is a shipped worker/v2 whose queue is polled is the lock's
|
|
553
|
+
* refinement, not restated here.
|
|
554
|
+
*/
|
|
555
|
+
function componentWorkflowClients(protocol, where) {
|
|
556
|
+
if (protocol.workflowClientOf === undefined) {return null;}
|
|
557
|
+
if (!Array.isArray(protocol.workflowClientOf)) {
|
|
558
|
+
throw new Error(`${where} protocol.workflowClientOf must be an array`);
|
|
559
|
+
}
|
|
560
|
+
if (protocol.workflowClientOf.length === 0) {return null;}
|
|
561
|
+
const schema = loadKernelBiomeContracts().BiomeWorkflowClientOfSchema;
|
|
562
|
+
return protocol.workflowClientOf.map((entry, index) => {
|
|
563
|
+
const parsed = schema.safeParse(entry);
|
|
564
|
+
if (!parsed.success) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`${where} has an invalid protocol.workflowClientOf[${index}]: ` +
|
|
567
|
+
parsed.error.issues.map((issue) => `[${issue.path.join('.')}] ${issue.message}`).join('; '),
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return parsed.data;
|
|
571
|
+
}).sort(
|
|
572
|
+
(a, b) =>
|
|
573
|
+
compareStrings(a.biome ?? '', b.biome ?? '') ||
|
|
574
|
+
compareStrings(a.component, b.component) ||
|
|
575
|
+
compareTaskQueueRefs(a.taskQueue, b.taskQueue),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* `{ imageOf, entry }` for a worker declaring `artifact.kind:
|
|
581
|
+
* shared-oci-image` — the sibling SERVICE whose sealed image it runs (by that
|
|
582
|
+
* service's image name, `protocol.serviceName`) and the module `node` runs in
|
|
583
|
+
* it — or `null` for any other component.
|
|
584
|
+
*
|
|
585
|
+
* The kernel manifest schema already refuses an inconsistent declaration; this
|
|
586
|
+
* projection reads the RAW manifest, so it re-checks exactly the facts it
|
|
587
|
+
* writes rather than trusting them: a lock naming a sibling that is not a
|
|
588
|
+
* platform-scheduled oci-image service would send the deploy plane to a
|
|
589
|
+
* subject that does not exist.
|
|
590
|
+
*/
|
|
591
|
+
function sharedImageWorker(component, components, where) {
|
|
592
|
+
if (component.artifact?.kind !== 'shared-oci-image') {return null;}
|
|
593
|
+
const siblingKey = component.artifact.component;
|
|
594
|
+
const sibling = components.find(
|
|
595
|
+
(candidate) =>
|
|
596
|
+
candidate !== null && typeof candidate === 'object' && candidate.key === siblingKey,
|
|
597
|
+
);
|
|
598
|
+
const imageOf = sibling?.protocol?.serviceName;
|
|
599
|
+
if (
|
|
600
|
+
sibling === undefined ||
|
|
601
|
+
sibling.kind !== 'service' ||
|
|
602
|
+
sibling.artifact?.kind !== 'oci-image' ||
|
|
603
|
+
sibling.scheduler !== 'platform' ||
|
|
604
|
+
typeof imageOf !== 'string' ||
|
|
605
|
+
imageOf.length === 0
|
|
606
|
+
) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`${where} runs the image of component "${String(siblingKey)}", which is not a ` +
|
|
609
|
+
`platform-scheduled service shipping its own oci-image in this biome`,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const entry = component.entrypoint?.module;
|
|
613
|
+
if (component.entrypoint?.kind !== 'node-module' || typeof entry !== 'string' || entry.length === 0) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`${where} declares a shared-oci-image artifact without a node-module entrypoint ` +
|
|
616
|
+
`naming the module to run`,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return { imageOf, entry };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function componentNexusDeclarations(component, name, protocol, where) {
|
|
623
|
+
if (protocol.nexus === undefined) {return { provides: [], requires: [] };}
|
|
624
|
+
if (component.kind !== 'worker') {
|
|
625
|
+
throw new Error(
|
|
626
|
+
`${where} declares protocol.nexus on a "${String(component.kind)}" component — ` +
|
|
627
|
+
`only a worker (protocol worker/v2) provides or requires a Nexus service`,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
const parsed = loadKernelBiomeContracts().BiomeWorkerProtocolSchema.safeParse(protocol);
|
|
631
|
+
if (!parsed.success) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`${where} is not a valid worker protocol: ` +
|
|
634
|
+
parsed.error.issues
|
|
635
|
+
.map((issue) => `[${issue.path.join('.')}] ${issue.message}`)
|
|
636
|
+
.join('; '),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const nexus = parsed.data.nexus ?? {};
|
|
640
|
+
return {
|
|
641
|
+
// `operations` travels with each row: the kernel's `resolveNexusBindings`
|
|
642
|
+
// binds a requirement only to a provider declaring EVERY operation it
|
|
643
|
+
// requires, so the operation names are binding facts, not decoration.
|
|
644
|
+
// Sorted by code unit, so the index is byte-stable whatever order the
|
|
645
|
+
// manifest lists them in.
|
|
646
|
+
provides: (nexus.provides ?? []).map((entry) => ({
|
|
647
|
+
component: name,
|
|
648
|
+
service: entry.service,
|
|
649
|
+
version: entry.version,
|
|
650
|
+
taskQueue: entry.taskQueue,
|
|
651
|
+
operations: entry.operations.map((operation) => operation.name).sort(compareStrings),
|
|
652
|
+
})),
|
|
653
|
+
requires: (nexus.requires ?? []).map((entry) => ({
|
|
654
|
+
component: name,
|
|
655
|
+
service: entry.service,
|
|
656
|
+
version: entry.version,
|
|
657
|
+
operations: [...entry.operations].sort(compareStrings),
|
|
658
|
+
...(entry.optional === true ? { optional: true } : {}),
|
|
659
|
+
})),
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const compareNexusRows = (a, b) =>
|
|
664
|
+
compareStrings(a.service, b.service) ||
|
|
665
|
+
compareStrings(a.version, b.version) ||
|
|
666
|
+
compareStrings(a.component, b.component);
|
|
667
|
+
|
|
668
|
+
const compareResourceRows = (a, b) =>
|
|
669
|
+
compareStrings(a.consumerService, b.consumerService) ||
|
|
670
|
+
compareStrings(a.kind, b.kind) ||
|
|
671
|
+
compareStrings(a.logicalName, b.logicalName);
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
const kernelDistributionDist = () => kernelDistPath('distribution', 'index.js');
|
|
676
|
+
|
|
677
|
+
// The kernel's process-topology module: the ONE trust-domain grammar
|
|
678
|
+
// (`deriveTrustCeiling`) a lock writer projects an edition's `trustCeiling`
|
|
679
|
+
// through. A sibling of the distribution dist, loaded beside it.
|
|
680
|
+
const kernelProcessTopologyDist = () => kernelDistPath('process-topology', 'index.js');
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Dynamically import the BUILT kernel distribution schemas. The kernel package
|
|
684
|
+
* is NOT linked into the repo-root node_modules (it lives in its own sub-
|
|
685
|
+
* workspace), so a bare `@xemahq/contracts/distribution` specifier does
|
|
686
|
+
* not resolve from root tooling. We import the deterministic dist path instead
|
|
687
|
+
* — fail-fast with an actionable message if the dist is not built.
|
|
688
|
+
*/
|
|
689
|
+
export async function loadKernelDistributionSchemas() {
|
|
690
|
+
assertKernelEditionProtocol(kernelPackageDir());
|
|
691
|
+
let mod;
|
|
692
|
+
try {
|
|
693
|
+
mod = await import(`file://${kernelDistributionDist()}`);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
throw new Error(
|
|
696
|
+
`Cannot load built kernel distribution schemas from ${kernelDistributionDist()}. ` +
|
|
697
|
+
`Build it first: run \`pnpm install --frozen-lockfile && pnpm run build\` ` +
|
|
698
|
+
`in the kernel checkout (${kernelRepoDir()}) — naming no package, deliberately. ` +
|
|
699
|
+
`Underlying error: ${err.message}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
const {
|
|
703
|
+
parseDistribution,
|
|
704
|
+
parseDistributionLock,
|
|
705
|
+
parseDistributionLockForReading,
|
|
706
|
+
parseUnsealedDistributionLock,
|
|
707
|
+
parseCustomerEditionDefinition,
|
|
708
|
+
customerTrustCeilingIssues,
|
|
709
|
+
sealDistributionLock,
|
|
710
|
+
unsealedProjection,
|
|
711
|
+
UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
|
|
712
|
+
DISTRIBUTION_LOCK_SCHEMA_VERSION,
|
|
713
|
+
computeBiomeContentHash,
|
|
714
|
+
computeBiomeSourceManifestHash,
|
|
715
|
+
parseBiomeSourceManifestHash,
|
|
716
|
+
parseOciDigest,
|
|
717
|
+
parseGitTreeId,
|
|
718
|
+
parseImageLock,
|
|
719
|
+
parseImageProducerStatement,
|
|
720
|
+
parseDistributionImagesStatement,
|
|
721
|
+
parseImageDeliveryEntitlementStatement,
|
|
722
|
+
parseDistributionImageEvidenceSet,
|
|
723
|
+
parseDistributionImageReplayState,
|
|
724
|
+
parseSigstoreOfflineTrustPolicy,
|
|
725
|
+
SigstoreEvidenceDescriptorSchema,
|
|
726
|
+
parsePlatformServiceSourceIndex,
|
|
727
|
+
parseRuntimeImageSourceIndex,
|
|
728
|
+
resolveRuntimeImagePlatforms,
|
|
729
|
+
BiomeInstallPolicy,
|
|
730
|
+
BiomeLaunchMode,
|
|
731
|
+
ImageSourceHashAlgorithm,
|
|
732
|
+
RuntimeImagePlatformKey,
|
|
733
|
+
RuntimeImageOwnerKind,
|
|
734
|
+
RuntimeImageSubjectKind,
|
|
735
|
+
IMAGE_PRODUCER_PREDICATE_TYPE,
|
|
736
|
+
DISTRIBUTION_IMAGES_PREDICATE_TYPE,
|
|
737
|
+
IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE,
|
|
738
|
+
EVIDENCE_CHANNELS,
|
|
739
|
+
EVIDENCE_CHANNEL_REFS,
|
|
740
|
+
resolveFirstPartyCapabilityDomains,
|
|
741
|
+
DistributionVisibility,
|
|
742
|
+
computeKernelContractAuthorityContentDigest,
|
|
743
|
+
KernelContractContentHashAlgorithm,
|
|
744
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_DIGEST_DOMAIN,
|
|
745
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_DIGEST_VERSION,
|
|
746
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR,
|
|
747
|
+
XEMA_KERNEL_CONTRACT_REPOSITORY_URL,
|
|
748
|
+
XEMA_KERNEL_CONTRACT_SOURCE_MODE,
|
|
749
|
+
XEMA_KERNEL_CONTRACT_SOURCE_PATHS,
|
|
750
|
+
compareLockedServiceResources,
|
|
751
|
+
resolveNexusBindings,
|
|
752
|
+
describeNexusBindingRefusal,
|
|
753
|
+
canonicalNexusFacts,
|
|
754
|
+
DistributionResolutionMode,
|
|
755
|
+
resolveBiomeClosure,
|
|
756
|
+
platformInvariantRoots,
|
|
757
|
+
BiomeClosureError,
|
|
758
|
+
ClosureEntityKind,
|
|
759
|
+
ClosureProvenanceKind,
|
|
760
|
+
describeProvenance,
|
|
761
|
+
describeHop,
|
|
762
|
+
deriveWorkflowSubjects,
|
|
763
|
+
} = mod;
|
|
764
|
+
let topology;
|
|
765
|
+
try {
|
|
766
|
+
topology = await import(`file://${kernelProcessTopologyDist()}`);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
throw new Error(
|
|
769
|
+
`Cannot load the built kernel process-topology module from ${kernelProcessTopologyDist()}. ` +
|
|
770
|
+
`Rebuild the kernel checkout (${kernelRepoDir()}). Underlying error: ${err.message}`,
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const { deriveTrustCeiling, defaultTrustDomain, FIRST_PARTY_TRUST_DOMAIN, ProcessTrustPolicySchema } = topology;
|
|
774
|
+
const { DistributionTrustPolicySchema } = mod;
|
|
775
|
+
if (
|
|
776
|
+
typeof parseDistribution !== 'function' ||
|
|
777
|
+
typeof parseDistributionLock !== 'function' ||
|
|
778
|
+
// The tolerant READER (`@xemahq/contracts` >= 0.27.0). Every PURE read of a
|
|
779
|
+
// lock in this repository goes through it; only the resolver, which WRITES
|
|
780
|
+
// the lock, uses the strict `parseDistributionLock`.
|
|
781
|
+
typeof parseDistributionLockForReading !== 'function' ||
|
|
782
|
+
// Lock v5 is the only grammar. The resolver writes the UNSEALED document,
|
|
783
|
+
// the seal mints each biomeContentHash (`computeBiomeContentHash`) and the
|
|
784
|
+
// kernel's `sealDistributionLock` attaches them; the CLI never assembles v5.
|
|
785
|
+
DISTRIBUTION_LOCK_SCHEMA_VERSION !== 5 ||
|
|
786
|
+
typeof parseUnsealedDistributionLock !== 'function' ||
|
|
787
|
+
typeof sealDistributionLock !== 'function' ||
|
|
788
|
+
typeof unsealedProjection !== 'function' ||
|
|
789
|
+
typeof UNSEALED_DISTRIBUTION_LOCK_SCHEMA !== 'string' ||
|
|
790
|
+
typeof computeBiomeContentHash !== 'function' ||
|
|
791
|
+
typeof computeBiomeSourceManifestHash !== 'function' ||
|
|
792
|
+
typeof parseBiomeSourceManifestHash !== 'function' ||
|
|
793
|
+
typeof parseOciDigest !== 'function' ||
|
|
794
|
+
typeof parseGitTreeId !== 'function' ||
|
|
795
|
+
typeof parseImageLock !== 'function' ||
|
|
796
|
+
typeof parseImageProducerStatement !== 'function' ||
|
|
797
|
+
typeof parseDistributionImagesStatement !== 'function' ||
|
|
798
|
+
typeof parseImageDeliveryEntitlementStatement !== 'function' ||
|
|
799
|
+
typeof parseDistributionImageEvidenceSet !== 'function' ||
|
|
800
|
+
typeof parseDistributionImageReplayState !== 'function' ||
|
|
801
|
+
typeof parseSigstoreOfflineTrustPolicy !== 'function' ||
|
|
802
|
+
typeof SigstoreEvidenceDescriptorSchema?.parse !== 'function' ||
|
|
803
|
+
typeof parsePlatformServiceSourceIndex !== 'function' ||
|
|
804
|
+
typeof parseRuntimeImageSourceIndex !== 'function' ||
|
|
805
|
+
typeof resolveRuntimeImagePlatforms !== 'function' ||
|
|
806
|
+
typeof resolveFirstPartyCapabilityDomains !== 'function' ||
|
|
807
|
+
typeof DistributionVisibility !== 'object' ||
|
|
808
|
+
typeof BiomeInstallPolicy !== 'object' ||
|
|
809
|
+
typeof BiomeLaunchMode !== 'object' ||
|
|
810
|
+
typeof ImageSourceHashAlgorithm !== 'object' ||
|
|
811
|
+
typeof RuntimeImagePlatformKey !== 'object' ||
|
|
812
|
+
typeof RuntimeImageOwnerKind !== 'object' ||
|
|
813
|
+
typeof RuntimeImageSubjectKind !== 'object' ||
|
|
814
|
+
typeof IMAGE_PRODUCER_PREDICATE_TYPE !== 'string' ||
|
|
815
|
+
typeof DISTRIBUTION_IMAGES_PREDICATE_TYPE !== 'string' ||
|
|
816
|
+
typeof IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE !== 'string' ||
|
|
817
|
+
!Array.isArray(EVIDENCE_CHANNELS) ||
|
|
818
|
+
typeof EVIDENCE_CHANNEL_REFS !== 'object' ||
|
|
819
|
+
typeof computeKernelContractAuthorityContentDigest !== 'function' ||
|
|
820
|
+
typeof KernelContractContentHashAlgorithm !== 'object' ||
|
|
821
|
+
typeof XEMA_KERNEL_CONTRACT_AUTHORITY_DIGEST_DOMAIN !== 'string' ||
|
|
822
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_DIGEST_VERSION !== 1 ||
|
|
823
|
+
typeof XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR !== 'string' ||
|
|
824
|
+
typeof XEMA_KERNEL_CONTRACT_REPOSITORY_URL !== 'string' ||
|
|
825
|
+
typeof XEMA_KERNEL_CONTRACT_SOURCE_MODE !== 'string' ||
|
|
826
|
+
!Array.isArray(XEMA_KERNEL_CONTRACT_SOURCE_PATHS) ||
|
|
827
|
+
!Object.isFrozen(XEMA_KERNEL_CONTRACT_SOURCE_PATHS) ||
|
|
828
|
+
// The ONE order a lock's `serviceResources` rows are emitted in — the
|
|
829
|
+
// comparator the kernel lock schema asserts, never a second spelling.
|
|
830
|
+
typeof compareLockedServiceResources !== 'function' ||
|
|
831
|
+
// THE Nexus binding rule — service, major version and every required
|
|
832
|
+
// operation — shared with the runtime resolver (>= 0.53.0).
|
|
833
|
+
typeof resolveNexusBindings !== 'function' ||
|
|
834
|
+
typeof describeNexusBindingRefusal !== 'function' ||
|
|
835
|
+
// The canonical per-biome Nexus facts a lock carries (>= 0.54.0).
|
|
836
|
+
typeof canonicalNexusFacts !== 'function' ||
|
|
837
|
+
// Positive closure (`@xemahq/contracts` >= the release that added the
|
|
838
|
+
// `resolution`/`roots` edition fields): the ONE closure algorithm, shared
|
|
839
|
+
// with runtime installation, and the vocabulary its provenance is written in.
|
|
840
|
+
typeof DistributionResolutionMode !== 'object' ||
|
|
841
|
+
typeof resolveBiomeClosure !== 'function' ||
|
|
842
|
+
typeof platformInvariantRoots !== 'function' ||
|
|
843
|
+
typeof BiomeClosureError !== 'function' ||
|
|
844
|
+
typeof ClosureEntityKind !== 'object' ||
|
|
845
|
+
typeof ClosureProvenanceKind !== 'object' ||
|
|
846
|
+
typeof describeProvenance !== 'function' ||
|
|
847
|
+
typeof describeHop !== 'function' ||
|
|
848
|
+
// A biome's workflow subjects and its edition trust ceiling, each written
|
|
849
|
+
// through the kernel's one derivation (`@xemahq/contracts` >= 0.71.0).
|
|
850
|
+
typeof deriveWorkflowSubjects !== 'function' ||
|
|
851
|
+
typeof deriveTrustCeiling !== 'function' ||
|
|
852
|
+
// The trust-domain vocabulary a customer edition is bounded by: the vendor's
|
|
853
|
+
// own domain, a biome's default domain, and the two ceiling grammars a
|
|
854
|
+
// catalogue carries.
|
|
855
|
+
typeof defaultTrustDomain !== 'function' ||
|
|
856
|
+
typeof FIRST_PARTY_TRUST_DOMAIN !== 'string' ||
|
|
857
|
+
typeof ProcessTrustPolicySchema?.safeParse !== 'function' ||
|
|
858
|
+
typeof DistributionTrustPolicySchema?.safeParse !== 'function'
|
|
859
|
+
) {
|
|
860
|
+
throw new Error(
|
|
861
|
+
`Built kernel distribution module at ${kernelDistributionDist()} does not export ` +
|
|
862
|
+
`parseDistribution/parseDistributionLock/parseDistributionLockForReading/DISTRIBUTION_LOCK_SCHEMA_VERSION/parseUnsealedDistributionLock/sealDistributionLock/unsealedProjection/computeBiomeContentHash/computeBiomeSourceManifestHash/parseImageLock/image-evidence parsers/` +
|
|
863
|
+
`parsePlatformServiceSourceIndex/parseRuntimeImageSourceIndex/` +
|
|
864
|
+
`resolveRuntimeImagePlatforms/RuntimeImagePlatformKey/` +
|
|
865
|
+
`kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
|
|
866
|
+
`positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
|
|
867
|
+
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/` +
|
|
868
|
+
`defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/` +
|
|
869
|
+
`resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
|
|
870
|
+
`BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
|
|
871
|
+
`the dist is stale; rebuild kernel-contracts.`,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
parseDistribution,
|
|
876
|
+
parseDistributionLock,
|
|
877
|
+
parseDistributionLockForReading,
|
|
878
|
+
parseUnsealedDistributionLock,
|
|
879
|
+
parseCustomerEditionDefinition,
|
|
880
|
+
customerTrustCeilingIssues,
|
|
881
|
+
sealDistributionLock,
|
|
882
|
+
unsealedProjection,
|
|
883
|
+
UNSEALED_DISTRIBUTION_LOCK_SCHEMA,
|
|
884
|
+
DISTRIBUTION_LOCK_SCHEMA_VERSION,
|
|
885
|
+
computeBiomeContentHash,
|
|
886
|
+
computeBiomeSourceManifestHash,
|
|
887
|
+
parseBiomeSourceManifestHash,
|
|
888
|
+
parseOciDigest,
|
|
889
|
+
parseGitTreeId,
|
|
890
|
+
parseImageLock,
|
|
891
|
+
parseImageProducerStatement,
|
|
892
|
+
parseDistributionImagesStatement,
|
|
893
|
+
parseImageDeliveryEntitlementStatement,
|
|
894
|
+
parseDistributionImageEvidenceSet,
|
|
895
|
+
parseDistributionImageReplayState,
|
|
896
|
+
parseSigstoreOfflineTrustPolicy,
|
|
897
|
+
SigstoreEvidenceDescriptorSchema,
|
|
898
|
+
parsePlatformServiceSourceIndex,
|
|
899
|
+
parseRuntimeImageSourceIndex,
|
|
900
|
+
resolveRuntimeImagePlatforms,
|
|
901
|
+
BiomeInstallPolicy,
|
|
902
|
+
BiomeLaunchMode,
|
|
903
|
+
ImageSourceHashAlgorithm,
|
|
904
|
+
RuntimeImagePlatformKey,
|
|
905
|
+
RuntimeImageOwnerKind,
|
|
906
|
+
RuntimeImageSubjectKind,
|
|
907
|
+
IMAGE_PRODUCER_PREDICATE_TYPE,
|
|
908
|
+
DISTRIBUTION_IMAGES_PREDICATE_TYPE,
|
|
909
|
+
IMAGE_DELIVERY_ENTITLEMENT_PREDICATE_TYPE,
|
|
910
|
+
EVIDENCE_CHANNELS,
|
|
911
|
+
EVIDENCE_CHANNEL_REFS,
|
|
912
|
+
resolveFirstPartyCapabilityDomains,
|
|
913
|
+
DistributionVisibility,
|
|
914
|
+
computeKernelContractAuthorityContentDigest,
|
|
915
|
+
KernelContractContentHashAlgorithm,
|
|
916
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_DIGEST_DOMAIN,
|
|
917
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_DIGEST_VERSION,
|
|
918
|
+
XEMA_KERNEL_CONTRACT_AUTHORITY_GENERATOR,
|
|
919
|
+
XEMA_KERNEL_CONTRACT_REPOSITORY_URL,
|
|
920
|
+
XEMA_KERNEL_CONTRACT_SOURCE_MODE,
|
|
921
|
+
XEMA_KERNEL_CONTRACT_SOURCE_PATHS,
|
|
922
|
+
compareLockedServiceResources,
|
|
923
|
+
resolveNexusBindings,
|
|
924
|
+
describeNexusBindingRefusal,
|
|
925
|
+
canonicalNexusFacts,
|
|
926
|
+
DistributionResolutionMode,
|
|
927
|
+
resolveBiomeClosure,
|
|
928
|
+
platformInvariantRoots,
|
|
929
|
+
BiomeClosureError,
|
|
930
|
+
ClosureEntityKind,
|
|
931
|
+
ClosureProvenanceKind,
|
|
932
|
+
describeProvenance,
|
|
933
|
+
describeHop,
|
|
934
|
+
deriveWorkflowSubjects,
|
|
935
|
+
deriveTrustCeiling,
|
|
936
|
+
defaultTrustDomain,
|
|
937
|
+
FIRST_PARTY_TRUST_DOMAIN,
|
|
938
|
+
ProcessTrustPolicySchema,
|
|
939
|
+
DistributionTrustPolicySchema,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* The published branch every image/platform source repository is pinned at.
|
|
945
|
+
*
|
|
946
|
+
* Deliberately NOT the local `HEAD`. A pin is a coordinate a CONSUMER has to be
|
|
947
|
+
* able to fetch: pinning a local HEAD records commits that exist only on this
|
|
948
|
+
* machine — an unpushed commit, or a branch someone is mid-way through — and a
|
|
949
|
+
* lock built on one is unresolvable anywhere else. It also made every generator
|
|
950
|
+
* hostage to the aggregator's checkout state, which is never quiescent.
|
|
951
|
+
*
|
|
952
|
+
* Every digest in release mode reads immutable Git objects at this exact
|
|
953
|
+
* commit, so the working tree is not an accidental input. Local development
|
|
954
|
+
* can opt into `workspace` authority explicitly; that mode also produces exact
|
|
955
|
+
* Git objects, but its synthetic commits are local-only and never selected by
|
|
956
|
+
* default.
|
|
957
|
+
*/
|
|
958
|
+
// Aligns with the CLI's manifest discovery rules:
|
|
959
|
+
// `fixtures`/`templates` hold test-fixture and scaffold `xema-biome.json` files
|
|
960
|
+
// that are NOT distributable biomes and must never enter the packaging index
|
|
961
|
+
// (e.g. create-biome templates, any package's `test/fixtures/*-biome`).
|
|
962
|
+
const SKIP_DIR_NAMES = new Set([
|
|
963
|
+
'node_modules',
|
|
964
|
+
'.git',
|
|
965
|
+
'dist',
|
|
966
|
+
'fixtures',
|
|
967
|
+
'templates',
|
|
968
|
+
]);
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Whether a discovered manifest path is an OpenAPI/test fixture rather than a
|
|
972
|
+
* real biome. Fixtures live under `scripts/openapi-fixture/` and re-use real
|
|
973
|
+
* biome ids (e.g. `runtime`), which would otherwise collide in the index.
|
|
974
|
+
*/
|
|
975
|
+
function isFixtureManifest(repoRelativePath) {
|
|
976
|
+
return repoRelativePath.split('/').includes('openapi-fixture');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export function distributionBiomeSourceRepositories() {
|
|
980
|
+
const publishersPath = join(
|
|
981
|
+
dataRoot(),
|
|
982
|
+
'store-publisher-identities.json',
|
|
983
|
+
);
|
|
984
|
+
const publishers = readStrictJsonFile(publishersPath);
|
|
985
|
+
if (
|
|
986
|
+
!Array.isArray(publishers.publishers) ||
|
|
987
|
+
publishers.publishers.length === 0
|
|
988
|
+
) {
|
|
989
|
+
throw new Error(`${publishersPath} declares no Store publishers`);
|
|
990
|
+
}
|
|
991
|
+
const repositoryNames = publishers.publishers.flatMap((publisher) => {
|
|
992
|
+
if (!Array.isArray(publisher.sourceRepositories)) {
|
|
993
|
+
throw new Error(
|
|
994
|
+
`${publishersPath} publisher ${String(publisher.anchorKey)} has no sourceRepositories`,
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
return publisher.sourceRepositories;
|
|
998
|
+
});
|
|
999
|
+
if (new Set(repositoryNames).size !== repositoryNames.length) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`${publishersPath} declares a source repository under more than one publisher`,
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return repositoryNames
|
|
1005
|
+
.map((repo) => ({ repo, directory: sourcePath(repo) }))
|
|
1006
|
+
.sort((a, b) => compareStrings(a.repo, b.repo));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Ceiling for one `git ls-tree -r` listing of a whole repository.
|
|
1011
|
+
*
|
|
1012
|
+
* Node's default is 1 MiB and this call outgrew it in production: `xema-base`
|
|
1013
|
+
* measured 1,056,095 bytes across 12,214 paths on 2026-08-20 — 7 KiB past the
|
|
1014
|
+
* default — so the biome-index step began failing with a bare
|
|
1015
|
+
* `spawnSync git ENOBUFS`, which names neither the repository nor the reason.
|
|
1016
|
+
* The listing grows with every file added to any carved repo, so the ceiling
|
|
1017
|
+
* has to sit far above today's number rather than just above it.
|
|
1018
|
+
*
|
|
1019
|
+
* A ceiling is safe to raise here because `execFileSync` THROWS on overflow
|
|
1020
|
+
* rather than truncating: exceeding it is a loud failure, never a short read
|
|
1021
|
+
* that would silently drop biomes from the index.
|
|
1022
|
+
*/
|
|
1023
|
+
const GIT_LS_TREE_MAX_BUFFER = 256 * 1024 * 1024;
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Every authoritative biome manifest in one repository AT ONE COMMIT, paired
|
|
1027
|
+
* with the `xema-biome.index.json` committed beside it.
|
|
1028
|
+
*
|
|
1029
|
+
* Extracted so the IMAGE-axis pass and the SOURCE-axis pass read manifests the
|
|
1030
|
+
* SAME way. Two copies of this walk would be two places for the skip rules, the
|
|
1031
|
+
* authoritative-source filter and the missing-index error to drift apart — and
|
|
1032
|
+
* the whole point of the split is that the two axes differ ONLY in which commit
|
|
1033
|
+
* they read.
|
|
1034
|
+
*
|
|
1035
|
+
* The manifest and its generated index are read from the SAME commit, which is
|
|
1036
|
+
* load-bearing and predates this split: the index is the authority for which
|
|
1037
|
+
* biomes this one requires, so reading them apart would let it describe a
|
|
1038
|
+
* different revision of the biome than the one being locked.
|
|
1039
|
+
*/
|
|
1040
|
+
function listBiomeManifestsAt(directory, repo, commit) {
|
|
1041
|
+
const found = [];
|
|
1042
|
+
const rawPaths = execFileSync(
|
|
1043
|
+
'git',
|
|
1044
|
+
['-C', directory, 'ls-tree', '-r', '-z', '--name-only', commit],
|
|
1045
|
+
{ encoding: 'buffer', maxBuffer: GIT_LS_TREE_MAX_BUFFER },
|
|
1046
|
+
);
|
|
1047
|
+
for (const repoPath of decodeNullDelimitedGitRecords(
|
|
1048
|
+
rawPaths,
|
|
1049
|
+
`Git manifest paths in ${repo}@${commit}`,
|
|
1050
|
+
)) {
|
|
1051
|
+
if (
|
|
1052
|
+
!repoPath.endsWith('/xema-biome.json') ||
|
|
1053
|
+
repoPath.split('/').some((segment) => SKIP_DIR_NAMES.has(segment)) ||
|
|
1054
|
+
isFixtureManifest(repoPath)
|
|
1055
|
+
) {
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
const manifestDirectory = repoPath.slice(0, -'/xema-biome.json'.length);
|
|
1059
|
+
const rel = `${repo}/${repoPath}`;
|
|
1060
|
+
const raw = readGitJsonAtCommit(directory, repoPath, commit);
|
|
1061
|
+
const path = `${repo}/${manifestDirectory}`;
|
|
1062
|
+
if (
|
|
1063
|
+
!isAuthoritativeProductionBiomeSource({
|
|
1064
|
+
id: raw?.xema?.id,
|
|
1065
|
+
repo,
|
|
1066
|
+
path,
|
|
1067
|
+
})
|
|
1068
|
+
) {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
// The generated index is COMMITTED beside the manifest and is the single
|
|
1072
|
+
// authority for which biomes this one requires. It is read from the SAME
|
|
1073
|
+
// pinned commit as the manifest, so the index cannot describe a different
|
|
1074
|
+
// revision of the biome than the one being locked.
|
|
1075
|
+
const indexRepoPath = `${manifestDirectory}/${BIOME_GENERATED_INDEX_FILENAME}`;
|
|
1076
|
+
let generatedIndex;
|
|
1077
|
+
try {
|
|
1078
|
+
generatedIndex = readGitJsonAtCommit(directory, indexRepoPath, commit);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`${repo}/${indexRepoPath} is missing at ${commit}: ${error.message}. ` +
|
|
1082
|
+
`Every biome carries a committed ${BIOME_GENERATED_INDEX_FILENAME} — it replaced the ` +
|
|
1083
|
+
`manifest field xema.requiresServerBiomes as the authority for required biomes. ` +
|
|
1084
|
+
`Run \`pnpm derive:biome-index:write\` in the aggregator and commit the result.`,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
// The biome's authorization module, read at the SAME pinned commit as the
|
|
1088
|
+
// manifest, so the sealer composes exactly the bytes the bundle signs.
|
|
1089
|
+
const authorizationModule = readBiomeAuthorizationModuleAt(
|
|
1090
|
+
manifestDirectory,
|
|
1091
|
+
(repoRelativePath) => readGitTextAtCommitOrNull(directory, repoRelativePath, commit),
|
|
1092
|
+
rel,
|
|
1093
|
+
);
|
|
1094
|
+
found.push({
|
|
1095
|
+
raw,
|
|
1096
|
+
generatedIndex,
|
|
1097
|
+
rel,
|
|
1098
|
+
repo,
|
|
1099
|
+
path,
|
|
1100
|
+
...(authorizationModule === null ? {} : { authorizationModule }),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return found;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** A file's text at a commit, or `null` when the commit does not carry it. */
|
|
1108
|
+
export function readGitTextAtCommitOrNull(repoDir, repoRelativePath, commit) {
|
|
1109
|
+
try {
|
|
1110
|
+
execFileSync('git', ['-C', repoDir, 'cat-file', '-e', `${commit}:${repoRelativePath}`], {
|
|
1111
|
+
stdio: 'ignore',
|
|
1112
|
+
});
|
|
1113
|
+
} catch {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return execFileSync('git', ['-C', repoDir, 'show', `${commit}:${repoRelativePath}`], {
|
|
1117
|
+
encoding: 'utf8',
|
|
1118
|
+
maxBuffer: GIT_LS_TREE_MAX_BUFFER,
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Does this biome ship a runtime image subject?
|
|
1124
|
+
*
|
|
1125
|
+
* THE axis discriminator, declared once. A biome that ships a service or a
|
|
1126
|
+
* runtime component earns image evidence, so every fact about it is pinned on
|
|
1127
|
+
* the IMAGE axis and verified there (`assembleProducerInput` recomputes its
|
|
1128
|
+
* `sourceManifestHash` at the image commit). A biome that ships neither can
|
|
1129
|
+
* never earn image evidence, so pinning it on the image axis makes it
|
|
1130
|
+
* permanently undeliverable — its content belongs on the SOURCE axis.
|
|
1131
|
+
*
|
|
1132
|
+
* Takes the `{ services, runtimeComponents }` shape that BOTH an index entry
|
|
1133
|
+
* (`manifestToIndexEntry`) and a LOCKED biome carry, so the two consumers that
|
|
1134
|
+
* must agree about this cannot drift: a second copy of the predicate is a
|
|
1135
|
+
* second answer to the question the whole two-axis split turns on.
|
|
1136
|
+
*/
|
|
1137
|
+
export function biomeShipsRuntimeImage(entry) {
|
|
1138
|
+
return (
|
|
1139
|
+
(entry.services?.length ?? 0) > 0 ||
|
|
1140
|
+
(entry.runtimeComponents?.length ?? 0) > 0
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function committedBiomeManifests(sourceRepositories, sourceAuthority) {
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
const repositories = sourceRepositories.map((repository) => ({
|
|
1148
|
+
...repository,
|
|
1149
|
+
commit: sourceAuthority.commitForRepository(repository.repo),
|
|
1150
|
+
// The SOURCE axis, for the second pass below.
|
|
1151
|
+
vendoredCommit: sourceAuthority.vendoredSourceCommitForRepository(repository.repo),
|
|
1152
|
+
}));
|
|
1153
|
+
const manifests = [];
|
|
1154
|
+
for (const { repo, directory, commit } of repositories) {
|
|
1155
|
+
manifests.push(...listBiomeManifestsAt(directory, repo, commit));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ── SECOND PASS: CONTENT-ONLY BIOMES ARE READ ON THE SOURCE AXIS ──
|
|
1159
|
+
//
|
|
1160
|
+
// The pass above reads every manifest at the IMAGE commit, and for a biome
|
|
1161
|
+
// that SHIPS A SERVICE that is exactly right: `resolve-images` pairs this
|
|
1162
|
+
// biome's `sourceManifestHash` with that repository's image commit, and
|
|
1163
|
+
// `assembleProducerInput` verifies the hash by recomputing it AT that commit.
|
|
1164
|
+
// Move those and every image-producing service fails verification.
|
|
1165
|
+
//
|
|
1166
|
+
// A CONTENT-ONLY biome has no image subject, so nothing verifies it — and
|
|
1167
|
+
// that same property is why it was undeliverable: it can never earn image
|
|
1168
|
+
// evidence, so the image-evidence commit could never carry its changes. Its
|
|
1169
|
+
// content belongs on the SOURCE axis, which is the commit whose bytes are
|
|
1170
|
+
// actually vendored into whichever image carries it.
|
|
1171
|
+
//
|
|
1172
|
+
// Measured 2026-09-14: `memory-maintenance` (services: [], runtimeComponents:
|
|
1173
|
+
// []) had migrated `modelClass` -> `modelLane`; the fix sat on cultivars' main
|
|
1174
|
+
// while the image pin was 130 commits behind, and biome-host crashlooped in
|
|
1175
|
+
// production on the retired key for 21 hours.
|
|
1176
|
+
//
|
|
1177
|
+
// Producing-ness is decided by PROJECTING the manifest through
|
|
1178
|
+
// `manifestToIndexEntry` rather than re-deriving it here — one authority for
|
|
1179
|
+
// "does this biome ship an image", not two that can disagree.
|
|
1180
|
+
//
|
|
1181
|
+
// This pass also lets a NEW content-only biome appear with no image build at
|
|
1182
|
+
// all, which is the other half of the same defect.
|
|
1183
|
+
let replaced = 0;
|
|
1184
|
+
let added = 0;
|
|
1185
|
+
for (const { repo, directory, commit, vendoredCommit } of repositories) {
|
|
1186
|
+
if (!vendoredCommit || vendoredCommit === commit) {continue;}
|
|
1187
|
+
for (const candidate of listBiomeManifestsAt(directory, repo, vendoredCommit)) {
|
|
1188
|
+
let entry;
|
|
1189
|
+
try {
|
|
1190
|
+
entry = await manifestToIndexEntry(candidate);
|
|
1191
|
+
} catch {
|
|
1192
|
+
// A manifest that will not project is not this pass's problem to
|
|
1193
|
+
// report: the image-axis pass above raises on it with its own message.
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (biomeShipsRuntimeImage(entry)) {continue;}
|
|
1197
|
+
const at = manifests.findIndex((existing) => existing.rel === candidate.rel);
|
|
1198
|
+
if (at === -1) {
|
|
1199
|
+
manifests.push(candidate);
|
|
1200
|
+
added += 1;
|
|
1201
|
+
} else {
|
|
1202
|
+
manifests[at] = candidate;
|
|
1203
|
+
replaced += 1;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
// A CORPUS LINE, because this pass is invisible in its output when the two
|
|
1208
|
+
// axes happen to agree — which is the normal case and was the case on the run
|
|
1209
|
+
// that introduced it: zero content-only manifests differed, so a no-op and a
|
|
1210
|
+
// working pass produce byte-identical indexes. Printing what it SELECTED is
|
|
1211
|
+
// the only thing that distinguishes them, and a future regression to
|
|
1212
|
+
// `replaced=0` is then visible rather than silent.
|
|
1213
|
+
console.log(
|
|
1214
|
+
`[dist:index] source axis: ${replaced} content-only biome(s) re-read at the ` +
|
|
1215
|
+
`vendored commit, ${added} added that exist only there ` +
|
|
1216
|
+
`(${repositories.filter((r) => r.vendoredCommit && r.vendoredCommit !== r.commit).length} ` +
|
|
1217
|
+
`repositor(y/ies) where the axes differ).`,
|
|
1218
|
+
);
|
|
1219
|
+
return manifests.sort((a, b) => compareStrings(a.rel, b.rel));
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Validate a service component's `protocol.databaseExtensions` the way the
|
|
1224
|
+
* kernel manifest schema does — an array of names matching the kernel's
|
|
1225
|
+
* extension-name GRAMMAR, with no repeats — and return it SORTED and
|
|
1226
|
+
* de-duplicated for the index. The predicate is read from the built kernel
|
|
1227
|
+
* dist so this plane cannot admit a name the kernel refuses, or refuse one it
|
|
1228
|
+
* admits.
|
|
1229
|
+
*
|
|
1230
|
+
* AN OPEN VOCABULARY, VALIDATED BY GRAMMAR — never a closed set. A PostgreSQL
|
|
1231
|
+
* extension name is owned by whoever runs the database, so a closed kernel enum
|
|
1232
|
+
* would produce members nothing can satisfy and would have to grow once per
|
|
1233
|
+
* customer: the same defect that deleted `DataResidency.Eu`. The kernel says so
|
|
1234
|
+
* itself — `PostgresExtensionNameSchema` is `z.string().regex(...)` and its
|
|
1235
|
+
* docstring reads "An OPEN vocabulary validated by GRAMMAR".
|
|
1236
|
+
*
|
|
1237
|
+
* This function previously read `Object.values(PostgresExtension)` and required
|
|
1238
|
+
* every entry to be a member. That export has never existed on any kernel
|
|
1239
|
+
* branch, so the biome-index build could not run at all while any manifest
|
|
1240
|
+
* declared the field, and its error blamed a stale dist. The consequence was
|
|
1241
|
+
* silent and total: the field reached ZERO of 62 subjects in the sealed lock.
|
|
1242
|
+
*
|
|
1243
|
+
* WHAT THIS DOES NOT DECIDE. Grammar admission is not installation permission.
|
|
1244
|
+
* Whether a deployment may install a given extension is
|
|
1245
|
+
* `infra.yaml#postgres.allowedExtensions`, enforced by the privileged bootstrap
|
|
1246
|
+
* — so a cloud tenant cannot install ad-hoc extensions even though the contract
|
|
1247
|
+
* can express any name. Contract breadth and runtime authority are separate
|
|
1248
|
+
* axes and this plane only carries the first.
|
|
1249
|
+
*
|
|
1250
|
+
* Throws naming the manifest and the component (`label`), because the index
|
|
1251
|
+
* build is where a manifest that would strand a database at boot should fail:
|
|
1252
|
+
* the runtime bootstrap only VERIFIES `pg_extension`, so a name nothing can
|
|
1253
|
+
* install is a pod that never comes up, discovered at the first deploy.
|
|
1254
|
+
*/
|
|
1255
|
+
export function parseDatabaseExtensions(value, label) {
|
|
1256
|
+
const { isPostgresExtensionName, POSTGRES_EXTENSION_NAME_PATTERN, POSTGRES_EXTENSION_NAME_MAX_LENGTH } =
|
|
1257
|
+
loadKernelBiomeContracts();
|
|
1258
|
+
const grammar =
|
|
1259
|
+
`${String(POSTGRES_EXTENSION_NAME_PATTERN)}, at most ` +
|
|
1260
|
+
`${POSTGRES_EXTENSION_NAME_MAX_LENGTH} characters`;
|
|
1261
|
+
if (!Array.isArray(value)) {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`${label} has protocol.databaseExtensions ${JSON.stringify(value)} — ` +
|
|
1264
|
+
`expected an array of PostgreSQL extension names (${grammar})`,
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const seen = new Set();
|
|
1268
|
+
for (const extension of value) {
|
|
1269
|
+
if (typeof extension !== 'string' || !isPostgresExtensionName(extension)) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`${label} has protocol.databaseExtensions entry ` +
|
|
1272
|
+
`${JSON.stringify(extension)} — not a PostgreSQL extension NAME the ` +
|
|
1273
|
+
`kernel grammar admits (${grammar}). The vocabulary is open, so this ` +
|
|
1274
|
+
`is a malformed name rather than an unlisted one; whether this ` +
|
|
1275
|
+
`deployment may install it is infra.yaml#postgres.allowedExtensions`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (seen.has(extension)) {
|
|
1279
|
+
throw new Error(
|
|
1280
|
+
`${label} repeats "${extension}" in protocol.databaseExtensions — ` +
|
|
1281
|
+
`an extension is declared once`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
seen.add(extension);
|
|
1285
|
+
}
|
|
1286
|
+
return [...seen].sort(compareStrings);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Read + minimally validate a raw `xema-biome.json` and project it into one
|
|
1291
|
+
* index entry. Fail-fast (throws) on missing id/scope/target or an unknown
|
|
1292
|
+
* tier. The full biome-manifest zod schema is NOT applied here — the index only
|
|
1293
|
+
* needs the packaging-relevant fields and must tolerate the wide variety of
|
|
1294
|
+
* (already-valid) manifests in the tree.
|
|
1295
|
+
*/
|
|
1296
|
+
/**
|
|
1297
|
+
* The process facts of ONE platform-scheduled image component, in the exact
|
|
1298
|
+
* shape the kernel's `LockedServiceProcessFactsSchema` carries:
|
|
1299
|
+
* `{ componentKind, scalingMode, processFacts? }`.
|
|
1300
|
+
*
|
|
1301
|
+
* The same three inputs the kernel's `deriveProcessMemberFacts` reads off a
|
|
1302
|
+
* component (`kind`, `requirements.scaling.mode`, `processFacts`) — the rest
|
|
1303
|
+
* of a member's facts (origin, version, build source) already travel in the
|
|
1304
|
+
* lock and are joined by the deploy plane. Nothing is defaulted: a missing or
|
|
1305
|
+
* unknown scaling class REFUSES, naming the manifest, because a guessed class
|
|
1306
|
+
* bounds a process's replicas and the kernel requires the field on every
|
|
1307
|
+
* component. `processFacts` is validated by the kernel's own schema so the
|
|
1308
|
+
* index can never carry a block the lock schema would then refuse.
|
|
1309
|
+
*/
|
|
1310
|
+
function componentProcessFacts(component, where) {
|
|
1311
|
+
const kernel = loadKernelBiomeContracts();
|
|
1312
|
+
const scalingMode = component.requirements?.scaling?.mode;
|
|
1313
|
+
if (!Object.values(kernel.BiomeScalingMode).includes(scalingMode)) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`${where} declares requirements.scaling.mode "${String(scalingMode)}" — ` +
|
|
1316
|
+
`expected one of ${Object.values(kernel.BiomeScalingMode).join(', ')}. ` +
|
|
1317
|
+
'The scaling class is what bounds a process\'s replicas, so it is read, never defaulted.',
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
const facts = { componentKind: component.kind, scalingMode };
|
|
1321
|
+
if (component.processFacts !== undefined) {
|
|
1322
|
+
const parsed = kernel.BiomeComponentProcessFactsSchema.safeParse(
|
|
1323
|
+
component.processFacts,
|
|
1324
|
+
);
|
|
1325
|
+
if (!parsed.success) {
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
`${where} declares an invalid processFacts block: ` +
|
|
1328
|
+
parsed.error.issues
|
|
1329
|
+
.map((issue) => `[${issue.path.join('.')}] ${issue.message}`)
|
|
1330
|
+
.join('; '),
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
facts.processFacts = parsed.data;
|
|
1334
|
+
}
|
|
1335
|
+
return facts;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, path, authorizationModule }) {
|
|
1339
|
+
const xema = raw?.xema;
|
|
1340
|
+
if (!xema || typeof xema !== 'object') {
|
|
1341
|
+
throw new Error(`${rel}: missing top-level "xema" object`);
|
|
1342
|
+
}
|
|
1343
|
+
const id = xema.id;
|
|
1344
|
+
const tier = xema.scope;
|
|
1345
|
+
const target = xema.target;
|
|
1346
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
1347
|
+
throw new Error(`${rel}: missing or empty xema.id`);
|
|
1348
|
+
}
|
|
1349
|
+
if (typeof tier !== 'string' || tier.length === 0) {
|
|
1350
|
+
throw new Error(`${rel}: missing or empty xema.scope (the biome tier)`);
|
|
1351
|
+
}
|
|
1352
|
+
if (!isBiomeTier(tier)) {
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`${rel}: xema.scope "${tier}" is not a valid BiomeTier (one of ${biomeTierOrder().join(', ')})`,
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (typeof target !== 'string' || target.length === 0) {
|
|
1358
|
+
throw new Error(`${rel}: missing or empty xema.target`);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const version = raw.version;
|
|
1362
|
+
if (typeof version !== 'string' || version.length === 0) {
|
|
1363
|
+
throw new Error(`${rel}: missing or empty top-level "version"`);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const origin = xema.trustTier ?? 'third-party';
|
|
1367
|
+
// `mandatory` is NOT a manifest field — it is a distribution decision applied
|
|
1368
|
+
// at resolve time from the `mandatory` selector flag (kernel-tier is always
|
|
1369
|
+
// mandatory). The index carries only manifest-intrinsic facts.
|
|
1370
|
+
const audience = xema.audience ?? 'org';
|
|
1371
|
+
// Resolved by the KERNEL predicate, never read raw — it applies two derived
|
|
1372
|
+
// refusals (operator audience, kernel tier) that a raw read silently skips.
|
|
1373
|
+
const storeListed = loadKernelBiomeContracts().isBiomeStoreListed(raw);
|
|
1374
|
+
|
|
1375
|
+
// Deployable service names this biome ships (manifest `ships.apis[].name`).
|
|
1376
|
+
// This is the biome→service mapping the deploy-roster generator needs; it is
|
|
1377
|
+
// baked into the index (and forwarded into the lock) because the generator
|
|
1378
|
+
// runs in a standalone checkout WITHOUT the biome sources. Pure skill/agent
|
|
1379
|
+
// biomes and web biomes have no API and resolve to [].
|
|
1380
|
+
const shipsApis = xema.ships?.apis;
|
|
1381
|
+
const legacyApis = Array.isArray(shipsApis) ? shipsApis : [];
|
|
1382
|
+
// component-contract-v5 shape: every image the biome ships is a
|
|
1383
|
+
// `xema.components[]` entry whose `kind` is "service" or "worker" and whose
|
|
1384
|
+
// `artifact.kind` is "oci-image". The remaining kinds (content, web, adapter,
|
|
1385
|
+
// and any host-module service) are loaded INTO another process and ship no
|
|
1386
|
+
// image of their own. This is DUAL SUPPORT for the staged ships.apis →
|
|
1387
|
+
// components migration, not a fallback.
|
|
1388
|
+
//
|
|
1389
|
+
// WHICH PLANE SCHEDULES the image is read from the component's `scheduler`
|
|
1390
|
+
// — the ONE owner of that fact, declared by the biome author. It is not
|
|
1391
|
+
// derivable from anything else here: the four `kind: "worker"` components in
|
|
1392
|
+
// xema-base are byte-identical in `entrypoint`, `protocol`, `executionModes`,
|
|
1393
|
+
// `requirements.scaling`, `requirements.io` and `requirements.state`, yet
|
|
1394
|
+
// three are standing Deployments and one must never be scheduled at all.
|
|
1395
|
+
//
|
|
1396
|
+
// Reading only `kind === "service"` (which this did until the contract regained
|
|
1397
|
+
// the discriminator) silently reclassified five image subjects: it dropped the
|
|
1398
|
+
// three platform-scheduled workers out of the roster entirely, dropped
|
|
1399
|
+
// `opencode-worker` out of the image plane altogether, and PROMOTED
|
|
1400
|
+
// `biome-adapter-host` to a rostered service that would have been given a
|
|
1401
|
+
// Deployment it must never have.
|
|
1402
|
+
const components = Array.isArray(xema.components) ? xema.components : [];
|
|
1403
|
+
const hostModuleComponents = components.filter(
|
|
1404
|
+
(component) =>
|
|
1405
|
+
component !== null &&
|
|
1406
|
+
typeof component === 'object' &&
|
|
1407
|
+
component.artifact?.kind === 'host-module',
|
|
1408
|
+
);
|
|
1409
|
+
const hostModules = hostModuleComponents
|
|
1410
|
+
.map((component) => {
|
|
1411
|
+
if (target !== 'server') {
|
|
1412
|
+
throw new Error(
|
|
1413
|
+
`${rel}: web biome "${id}" cannot ship host module "${String(component.key)}"`,
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
if (!xema.bundleSource || typeof xema.bundleSource !== 'object') {
|
|
1417
|
+
throw new Error(
|
|
1418
|
+
`${rel}: host module "${String(component.key)}" requires xema.bundleSource; ` +
|
|
1419
|
+
'a distribution host cannot fetch an executable from catalog prose or a source-tree path',
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (
|
|
1423
|
+
component.entrypoint?.kind !== 'module-export' ||
|
|
1424
|
+
typeof component.entrypoint.module !== 'string' ||
|
|
1425
|
+
component.entrypoint.module.length === 0 ||
|
|
1426
|
+
typeof component.entrypoint.exportName !== 'string' ||
|
|
1427
|
+
component.entrypoint.exportName.length === 0
|
|
1428
|
+
) {
|
|
1429
|
+
throw new Error(
|
|
1430
|
+
`${rel}: host module "${String(component.key)}" must declare a non-empty module-export entrypoint`,
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (
|
|
1434
|
+
typeof component.artifact.path !== 'string' ||
|
|
1435
|
+
component.artifact.path.length === 0 ||
|
|
1436
|
+
typeof component.hostAbi?.name !== 'string' ||
|
|
1437
|
+
component.hostAbi.name.length === 0 ||
|
|
1438
|
+
typeof component.hostAbi?.versionRange !== 'string' ||
|
|
1439
|
+
component.hostAbi.versionRange.length === 0
|
|
1440
|
+
) {
|
|
1441
|
+
throw new Error(
|
|
1442
|
+
`${rel}: host module "${String(component.key)}" must declare artifact.path and hostAbi`,
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
return {
|
|
1446
|
+
componentKey: component.key,
|
|
1447
|
+
artifactPath: component.artifact.path,
|
|
1448
|
+
modulePath: component.entrypoint.module,
|
|
1449
|
+
exportName: component.entrypoint.exportName,
|
|
1450
|
+
hostAbi: {
|
|
1451
|
+
name: component.hostAbi.name,
|
|
1452
|
+
versionRange: component.hostAbi.versionRange,
|
|
1453
|
+
},
|
|
1454
|
+
bundleSource: xema.bundleSource,
|
|
1455
|
+
};
|
|
1456
|
+
})
|
|
1457
|
+
.sort((left, right) => compareStrings(left.componentKey, right.componentKey));
|
|
1458
|
+
// The BIOME-LEVEL bundle source — `xema.bundleSource`, WHERE the runtime
|
|
1459
|
+
// fetches this biome's bundle — for a biome whose bundle nothing above
|
|
1460
|
+
// already names.
|
|
1461
|
+
//
|
|
1462
|
+
// Until this existed the manifest's declaration reached the lock in exactly
|
|
1463
|
+
// one place: `hostModules[].bundleSource`, once per host module, because the
|
|
1464
|
+
// only runtime consumer that fetched a distribution-selected bundle was the
|
|
1465
|
+
// shared adapter host. A biome shipping `components[].kind = service` and no
|
|
1466
|
+
// host module — the `pai-core` / `document-translation` shape in the
|
|
1467
|
+
// customer edition — declared an `oci` source in its
|
|
1468
|
+
// manifest and reached biome-host with NONE, so the required-biome
|
|
1469
|
+
// reconciler reported it UNRESOLVABLE. Nothing at the runtime may guess the
|
|
1470
|
+
// coordinate instead: `xema biome publish` pushes to the manifest's declared
|
|
1471
|
+
// `ociRef` while `bundle-coordinate.mjs` derives `<registry>/<id>:<hash>`,
|
|
1472
|
+
// and a third rule matching one of them today fetches a DIFFERENT signed
|
|
1473
|
+
// bundle by the same publisher the day either moves, and fails nothing.
|
|
1474
|
+
//
|
|
1475
|
+
// STATED ONCE PER BIOME, and that is the byte-stability property: a biome
|
|
1476
|
+
// that ships host modules keeps its source on each host module, unchanged,
|
|
1477
|
+
// and gains NO biome-level key. Measured at `origin/develop`: the seventeen
|
|
1478
|
+
// manifests that declare `xema.bundleSource` are all connector biomes with a
|
|
1479
|
+
// host module, so every committed index entry and all nine committed vendor
|
|
1480
|
+
// locks are byte-identical under this projection. Emitting it for those too
|
|
1481
|
+
// would be the same value stated twice and would re-stamp five vendor locks
|
|
1482
|
+
// to lock schemaVersion 4 — a fleet rollout, not a projection. A reader
|
|
1483
|
+
// that wants one answer for any biome reads the biome-level field first and
|
|
1484
|
+
// falls back to the host-module projection (biome-host's
|
|
1485
|
+
// `resolveLockedBiomeBundleSource`).
|
|
1486
|
+
//
|
|
1487
|
+
// Validated by the KERNEL's own `BiomeBundleSourceSpecSchema` — the one
|
|
1488
|
+
// shape the lock reuses — so a malformed declaration fails HERE, naming the
|
|
1489
|
+
// manifest, rather than at `parseDistributionLock` forty fields in.
|
|
1490
|
+
let bundleSource;
|
|
1491
|
+
if (xema.bundleSource !== undefined && hostModules.length === 0) {
|
|
1492
|
+
const parsed = loadKernelBiomeContracts().BiomeBundleSourceSpecSchema.safeParse(
|
|
1493
|
+
xema.bundleSource,
|
|
1494
|
+
);
|
|
1495
|
+
if (!parsed.success) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`${rel}: xema.bundleSource is not a valid biome bundle source: ` +
|
|
1498
|
+
parsed.error.issues
|
|
1499
|
+
.map((issue) => `[${issue.path.join('.')}] ${issue.message}`)
|
|
1500
|
+
.join('; '),
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
bundleSource = parsed.data;
|
|
1504
|
+
}
|
|
1505
|
+
const imageComponents = components.filter(
|
|
1506
|
+
(component) =>
|
|
1507
|
+
component !== null &&
|
|
1508
|
+
typeof component === 'object' &&
|
|
1509
|
+
(component.kind === 'service' || component.kind === 'worker') &&
|
|
1510
|
+
(component.artifact?.kind === 'oci-image' ||
|
|
1511
|
+
// A worker running its sibling service's image with its own entry
|
|
1512
|
+
// module is a standing workload like any other image component — it is
|
|
1513
|
+
// rostered, has process facts, task queues and peers — it just builds
|
|
1514
|
+
// no image of its own. `workerImages` below records whose it runs.
|
|
1515
|
+
(component.kind === 'worker' &&
|
|
1516
|
+
component.artifact?.kind === 'shared-oci-image')),
|
|
1517
|
+
);
|
|
1518
|
+
// A worker is a standing workload that runs an image: its own oci-image or a
|
|
1519
|
+
// sibling service's (shared-oci-image, validated by `sharedImageWorker`).
|
|
1520
|
+
// A worker with neither would be rostered by nothing and ship no image.
|
|
1521
|
+
const imagelessWorkers = components.filter(
|
|
1522
|
+
(component) =>
|
|
1523
|
+
component !== null &&
|
|
1524
|
+
typeof component === 'object' &&
|
|
1525
|
+
component.kind === 'worker' &&
|
|
1526
|
+
!imageComponents.includes(component),
|
|
1527
|
+
);
|
|
1528
|
+
if (imagelessWorkers.length > 0) {
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
`${rel}: worker component(s) ${imagelessWorkers.map((component) => JSON.stringify(component.name ?? component.key ?? '?')).join(', ')} ` +
|
|
1531
|
+
'resolve no worker image: a worker declares artifact.kind "oci-image" or "shared-oci-image"',
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
// A biome must declare its images in EXACTLY ONE shape. Declaring both a
|
|
1535
|
+
// legacy `ships.apis[]` entry and a v5 image component is ambiguous during
|
|
1536
|
+
// the staged migration — fail fast instead of guessing.
|
|
1537
|
+
if (legacyApis.length > 0 && imageComponents.length > 0) {
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
`${rel}: declares deployable images in BOTH xema.ships.apis[] (legacy) and ` +
|
|
1540
|
+
`xema.components[] (component-contract-v5) — a biome must use exactly one shape`,
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const serviceRequirements = [];
|
|
1544
|
+
const services = [];
|
|
1545
|
+
// `service → database key` for the few services whose key is not derivable.
|
|
1546
|
+
const serviceDatabases = {};
|
|
1547
|
+
// `service → PostgreSQL extensions its database must carry`, for the few
|
|
1548
|
+
// services that declare any. Travels beside `serviceDatabases` into the lock,
|
|
1549
|
+
// where the deployment's privileged database bootstrap reads it.
|
|
1550
|
+
const serviceDatabaseExtensions = {};
|
|
1551
|
+
// Images the biome ships that are never scheduled (`workload:
|
|
1552
|
+
// "runtime-component"`). They are built, content-addressed, signed and
|
|
1553
|
+
// mirrored exactly like a service image, but the deploy plane must never
|
|
1554
|
+
// roster a release for them — so they are carried in a SEPARATE array and
|
|
1555
|
+
// contribute NO service-dependency edge and NO service owner.
|
|
1556
|
+
const runtimeComponents = [];
|
|
1557
|
+
// Infrastructure resources each rostered component NEEDS
|
|
1558
|
+
// (`protocol.resources`) and the resource kinds it PROVIDES
|
|
1559
|
+
// (`protocol.providesResources`). Declared INTENT only: the resolver binds
|
|
1560
|
+
// each requirement to the one provider of its kind in the edition, and the
|
|
1561
|
+
// lock carries that binding as an authorization input the provider enforces.
|
|
1562
|
+
// Validated by the kernel's own schemas, at index build, naming the manifest.
|
|
1563
|
+
const serviceResources = [];
|
|
1564
|
+
const providesResources = [];
|
|
1565
|
+
// `service → what sharing a process needs to know about it`, for EVERY
|
|
1566
|
+
// platform-scheduled image component — see `componentProcessFacts`.
|
|
1567
|
+
const serviceProcessFacts = {};
|
|
1568
|
+
// Nexus services this biome's workers PROVIDE and REQUIRE (worker protocol
|
|
1569
|
+
// v2). Declared intent only, exactly like resources: the edition resolver
|
|
1570
|
+
// binds each requirement to the ONE shipped provider of the same
|
|
1571
|
+
// (service, version) and seals the answer in the lock's `nexusBindings`.
|
|
1572
|
+
const nexusProvides = [];
|
|
1573
|
+
const nexusRequires = [];
|
|
1574
|
+
// `component → its task queues`, for every protocol worker/v2 component —
|
|
1575
|
+
// see `componentWorkerTaskQueues`.
|
|
1576
|
+
const workerTaskQueues = {};
|
|
1577
|
+
// `service → the worker queues it starts workflows on` — see
|
|
1578
|
+
// `componentWorkflowClients`.
|
|
1579
|
+
const workflowClients = {};
|
|
1580
|
+
// `worker → { imageOf, entry }` for every worker that runs a sibling
|
|
1581
|
+
// service's image — see `sharedImageWorker`.
|
|
1582
|
+
const workerImages = {};
|
|
1583
|
+
if (imageComponents.length > 0) {
|
|
1584
|
+
for (const component of imageComponents) {
|
|
1585
|
+
const protocol =
|
|
1586
|
+
component.protocol !== null && typeof component.protocol === 'object'
|
|
1587
|
+
? component.protocol
|
|
1588
|
+
: {};
|
|
1589
|
+
// A `service` registers under `protocol.serviceName`; a `worker` never
|
|
1590
|
+
// registers with the service registry, so its image name is the component
|
|
1591
|
+
// key. Same rule the producing repo's build plane applies — the image name
|
|
1592
|
+
// is the SERVICE build arg either way.
|
|
1593
|
+
const name =
|
|
1594
|
+
component.kind === 'service' ? protocol.serviceName : component.key;
|
|
1595
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
1596
|
+
throw new Error(
|
|
1597
|
+
`${rel}: xema.components[] kind "${component.kind}" component ` +
|
|
1598
|
+
`"${String(component.key)}" has no image name — a service declares ` +
|
|
1599
|
+
`protocol.serviceName, a worker a non-empty key`,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
if (target !== 'server') {
|
|
1603
|
+
throw new Error(
|
|
1604
|
+
`${rel}: web biome "${id}" cannot own deployable image "${name}"`,
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
if (services.includes(name) || runtimeComponents.includes(name)) {
|
|
1608
|
+
throw new Error(
|
|
1609
|
+
`${rel}: xema.components[] declares image "${name}" more than once`,
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
const where = `${rel}: xema.components[] "${name}"`;
|
|
1613
|
+
const requirements = parseComponentResourceDeclaration(
|
|
1614
|
+
'ResourceRequirementsSchema',
|
|
1615
|
+
protocol.resources,
|
|
1616
|
+
`${where} protocol.resources`,
|
|
1617
|
+
);
|
|
1618
|
+
const provisions = parseComponentResourceDeclaration(
|
|
1619
|
+
'ResourceProvisionsSchema',
|
|
1620
|
+
protocol.providesResources,
|
|
1621
|
+
`${where} protocol.providesResources`,
|
|
1622
|
+
);
|
|
1623
|
+
const nexus = componentNexusDeclarations(component, name, protocol, where);
|
|
1624
|
+
if (component.scheduler === 'runtime') {
|
|
1625
|
+
// A Nexus binding names the provider COMPONENT the deploy plane points
|
|
1626
|
+
// an endpoint at, which must be a rostered image; a component launched
|
|
1627
|
+
// per allocation is not one. Refused rather than dropped.
|
|
1628
|
+
if (nexus.provides.length > 0 || nexus.requires.length > 0) {
|
|
1629
|
+
throw new Error(
|
|
1630
|
+
`${where} declares scheduler "runtime" and protocol.nexus — a Nexus ` +
|
|
1631
|
+
`binding names a rostered worker, and a component that is never ` +
|
|
1632
|
+
`scheduled is not one`,
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
// A resource binding is admitted against the caller's VERIFIED SERVICE
|
|
1636
|
+
// identity, and a runtime component — launched per allocation — is not
|
|
1637
|
+
// a rostered service that identity could name. Refused rather than
|
|
1638
|
+
// dropped: a skipped declaration would reach no lock and fail nothing.
|
|
1639
|
+
if (requirements.length > 0 || provisions.length > 0) {
|
|
1640
|
+
throw new Error(
|
|
1641
|
+
`${where} declares scheduler "runtime" and protocol.resources / ` +
|
|
1642
|
+
`protocol.providesResources — a resource binding names a rostered ` +
|
|
1643
|
+
`service, and a component that is never scheduled is not one`,
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
// Never scheduled, so `requiresServices` — an edge between two ROSTERED
|
|
1647
|
+
// services — is meaningless on it, exactly as it was for the legacy
|
|
1648
|
+
// `workload: "runtime-component"` shape.
|
|
1649
|
+
if (
|
|
1650
|
+
Array.isArray(protocol.requiresServices) &&
|
|
1651
|
+
protocol.requiresServices.length > 0
|
|
1652
|
+
) {
|
|
1653
|
+
throw new Error(
|
|
1654
|
+
`${rel}: xema.components[] "${name}" declares scheduler "runtime" and ` +
|
|
1655
|
+
`protocol.requiresServices — a component that is never scheduled ` +
|
|
1656
|
+
`cannot carry a service-dependency edge`,
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
runtimeComponents.push(name);
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
if (component.scheduler !== 'platform') {
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
`${rel}: xema.components[] "${name}" declares scheduler ` +
|
|
1665
|
+
`"${String(component.scheduler)}" — every image component must declare ` +
|
|
1666
|
+
`"platform" (the deploy plane rosters a standing workload) or "runtime" ` +
|
|
1667
|
+
`(the runtime plane launches it on demand)`,
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
const requiredServices = protocol.requiresServices ?? [];
|
|
1671
|
+
if (
|
|
1672
|
+
!Array.isArray(requiredServices) ||
|
|
1673
|
+
requiredServices.some(
|
|
1674
|
+
(service) => typeof service !== 'string' || service.length === 0,
|
|
1675
|
+
)
|
|
1676
|
+
) {
|
|
1677
|
+
throw new Error(
|
|
1678
|
+
`${rel}: xema.components[] "${name}" protocol.requiresServices must be an ` +
|
|
1679
|
+
`array of non-empty service names`,
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
// The shared-biome-cluster database key this service opens, declared ONLY
|
|
1683
|
+
// when it is neither the biome id nor the service name minus `-api`.
|
|
1684
|
+
// org-database-pool-api's ownership fence can derive those two and nothing
|
|
1685
|
+
// else, so a service whose schema name outlived a rename
|
|
1686
|
+
// (`scm-connector-api` → schema `scm-integration`) is unreachable unless
|
|
1687
|
+
// the component says so. The v5 spelling is `protocol.databaseKey`; the
|
|
1688
|
+
// legacy one below is `ships.apis[].database`.
|
|
1689
|
+
if (protocol.databaseKey !== undefined) {
|
|
1690
|
+
if (
|
|
1691
|
+
typeof protocol.databaseKey !== 'string' ||
|
|
1692
|
+
!/^[a-z][a-z0-9-]*$/.test(protocol.databaseKey)
|
|
1693
|
+
) {
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
`${rel}: xema.components[] "${name}" has protocol.databaseKey ` +
|
|
1696
|
+
`"${String(protocol.databaseKey)}" — expected lowercase kebab-case ` +
|
|
1697
|
+
`(it becomes a Postgres schema name)`,
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
serviceDatabases[name] = protocol.databaseKey;
|
|
1701
|
+
}
|
|
1702
|
+
// The PostgreSQL extensions this service's database must carry BEFORE its
|
|
1703
|
+
// first migration runs (`protocol.databaseExtensions`, the sibling of
|
|
1704
|
+
// `databaseKey`). A database-level prerequisite the deployment's
|
|
1705
|
+
// privileged bootstrap satisfies, never something the service creates —
|
|
1706
|
+
// so it has to travel from the manifest to the lock, which is the one
|
|
1707
|
+
// artifact the deploy plane reads without the biome sources. Validated
|
|
1708
|
+
// here exactly as the kernel manifest schema does (closed set, no
|
|
1709
|
+
// repeats), because this projection reads the RAW manifest and the full
|
|
1710
|
+
// kernel schema is deliberately not applied on this path. ABSENT and
|
|
1711
|
+
// `[]` make the same claim — no extension — and project to no key.
|
|
1712
|
+
//
|
|
1713
|
+
// Deliberately NO legacy `ships.apis[]` spelling: that shape predates the
|
|
1714
|
+
// field, and every service that has an extension requirement is on the
|
|
1715
|
+
// component contract.
|
|
1716
|
+
if (protocol.databaseExtensions !== undefined) {
|
|
1717
|
+
const extensions = parseDatabaseExtensions(
|
|
1718
|
+
protocol.databaseExtensions,
|
|
1719
|
+
`${rel}: xema.components[] "${name}"`,
|
|
1720
|
+
);
|
|
1721
|
+
if (extensions.length > 0) {
|
|
1722
|
+
serviceDatabaseExtensions[name] = extensions;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
for (const requirement of requirements) {
|
|
1726
|
+
serviceResources.push({
|
|
1727
|
+
consumerService: name,
|
|
1728
|
+
kind: requirement.kind,
|
|
1729
|
+
logicalName: requirement.logicalName,
|
|
1730
|
+
scope: requirement.scope,
|
|
1731
|
+
inclusion: requirement.inclusion,
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
for (const provision of provisions) {
|
|
1735
|
+
providesResources.push({ service: name, kind: provision.kind });
|
|
1736
|
+
}
|
|
1737
|
+
nexusProvides.push(...nexus.provides);
|
|
1738
|
+
nexusRequires.push(...nexus.requires);
|
|
1739
|
+
services.push(name);
|
|
1740
|
+
serviceProcessFacts[name] = componentProcessFacts(component, where);
|
|
1741
|
+
const queues = componentWorkerTaskQueues(component, protocol, where);
|
|
1742
|
+
if (queues !== null) {workerTaskQueues[name] = queues;}
|
|
1743
|
+
const clients = componentWorkflowClients(protocol, where);
|
|
1744
|
+
if (clients !== null) {workflowClients[name] = clients;}
|
|
1745
|
+
const shared = sharedImageWorker(component, components, where);
|
|
1746
|
+
if (shared !== null) {workerImages[name] = shared;}
|
|
1747
|
+
serviceRequirements.push({
|
|
1748
|
+
consumerService: name,
|
|
1749
|
+
requiredServices: [...new Set(requiredServices)].sort(compareStrings),
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
} else {
|
|
1753
|
+
for (const api of legacyApis) {
|
|
1754
|
+
if (!api || typeof api.name !== 'string' || api.name.length === 0) {
|
|
1755
|
+
throw new Error(
|
|
1756
|
+
`${rel}: xema.ships.apis[] entry is missing a non-empty "name"`,
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
if (target !== 'server') {
|
|
1760
|
+
throw new Error(
|
|
1761
|
+
`${rel}: web biome "${id}" cannot own deployable API service "${api.name}"`,
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
const workload = api.workload ?? DEFAULT_BIOME_WORKLOAD_KIND;
|
|
1765
|
+
if (!BIOME_WORKLOAD_KINDS.has(workload)) {
|
|
1766
|
+
throw new Error(
|
|
1767
|
+
`${rel}: xema.ships.apis[] "${api.name}" has unknown workload ` +
|
|
1768
|
+
`"${String(workload)}" — expected one of ${[...BIOME_WORKLOAD_KINDS].join(', ')}`,
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
if (workload === 'runtime-component') {
|
|
1772
|
+
// A runtime component has no standing workload, so `requiresServices`
|
|
1773
|
+
// (an edge between two ROSTERED services) is meaningless on it.
|
|
1774
|
+
if (
|
|
1775
|
+
Array.isArray(api.requiresServices) &&
|
|
1776
|
+
api.requiresServices.length > 0
|
|
1777
|
+
) {
|
|
1778
|
+
throw new Error(
|
|
1779
|
+
`${rel}: xema.ships.apis[] "${api.name}" declares workload ` +
|
|
1780
|
+
`"runtime-component" and requiresServices — a component that is never ` +
|
|
1781
|
+
`scheduled cannot carry a service-dependency edge`,
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
runtimeComponents.push(api.name);
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
const requiredServices = api.requiresServices ?? [];
|
|
1788
|
+
if (
|
|
1789
|
+
!Array.isArray(requiredServices) ||
|
|
1790
|
+
requiredServices.some(
|
|
1791
|
+
(service) => typeof service !== 'string' || service.length === 0,
|
|
1792
|
+
)
|
|
1793
|
+
) {
|
|
1794
|
+
throw new Error(
|
|
1795
|
+
`${rel}: xema.ships.apis[] "${api.name}" requiresServices must be an array of non-empty service names`,
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
services.push(api.name);
|
|
1799
|
+
// The shared-biome-cluster database key this service opens, declared ONLY
|
|
1800
|
+
// when it is neither the biome id nor the service name minus `-api`.
|
|
1801
|
+
// org-database-pool-api's ownership fence can derive those two and
|
|
1802
|
+
// nothing else, so a service whose schema name outlived a rename
|
|
1803
|
+
// (`scm-connector-api` → schema `scm-integration`) is unreachable unless
|
|
1804
|
+
// the biome says so here. Carried into the lock, which is where the fence
|
|
1805
|
+
// reads it — a distribution-time fact, never a request-time claim.
|
|
1806
|
+
if (api.database !== undefined) {
|
|
1807
|
+
if (typeof api.database !== 'string' || !/^[a-z][a-z0-9-]*$/.test(api.database)) {
|
|
1808
|
+
throw new Error(
|
|
1809
|
+
`${rel}: xema.ships.apis[] "${api.name}" has database ` +
|
|
1810
|
+
`"${String(api.database)}" — expected lowercase kebab-case (it becomes a Postgres schema name)`,
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
serviceDatabases[api.name] = api.database;
|
|
1814
|
+
}
|
|
1815
|
+
serviceRequirements.push({
|
|
1816
|
+
consumerService: api.name,
|
|
1817
|
+
requiredServices: [...new Set(requiredServices)].sort(compareStrings),
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
services.sort(compareStrings);
|
|
1822
|
+
runtimeComponents.sort(compareStrings);
|
|
1823
|
+
serviceRequirements.sort((a, b) =>
|
|
1824
|
+
compareStrings(a.consumerService, b.consumerService),
|
|
1825
|
+
);
|
|
1826
|
+
serviceResources.sort(compareResourceRows);
|
|
1827
|
+
nexusProvides.sort(compareNexusRows);
|
|
1828
|
+
nexusRequires.sort(compareNexusRows);
|
|
1829
|
+
providesResources.sort(
|
|
1830
|
+
(a, b) => compareStrings(a.service, b.service) || compareStrings(a.kind, b.kind),
|
|
1831
|
+
);
|
|
1832
|
+
// The provider derives a resource's PHYSICAL identity from the owning biome
|
|
1833
|
+
// plus (kind, scope, logicalName). Two consumers of this biome naming the
|
|
1834
|
+
// same tuple would be handed ONE physical resource while each believes it
|
|
1835
|
+
// owns it — so the declaration is refused here, where the biome is the whole
|
|
1836
|
+
// subject, rather than discovered at the provider.
|
|
1837
|
+
const physicalResources = new Map();
|
|
1838
|
+
for (const row of serviceResources) {
|
|
1839
|
+
const identity = `${row.kind}\0${row.scope}\0${row.logicalName}`;
|
|
1840
|
+
const holder = physicalResources.get(identity);
|
|
1841
|
+
if (holder !== undefined && holder !== row.consumerService) {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
`${rel}: services "${holder}" and "${row.consumerService}" both declare ` +
|
|
1844
|
+
`${row.scope}-scoped ${row.kind} "${row.logicalName}" — biome "${id}" ` +
|
|
1845
|
+
`derives ONE physical resource from that, so two consumers would share ` +
|
|
1846
|
+
`it while each believes it owns it. Give each its own logicalName.`,
|
|
1847
|
+
);
|
|
1848
|
+
}
|
|
1849
|
+
physicalResources.set(identity, row.consumerService);
|
|
1850
|
+
}
|
|
1851
|
+
// The two arrays partition the biome's images — a name in both would be
|
|
1852
|
+
// simultaneously rostered and unschedulable.
|
|
1853
|
+
for (const name of runtimeComponents) {
|
|
1854
|
+
if (services.includes(name)) {
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
`${rel}: xema.ships.apis[] "${name}" is declared both as a service and ` +
|
|
1857
|
+
`as a runtime component — an image is either scheduled or it is not`,
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Biomes this one REQUIRES, read from the committed generated index rather
|
|
1863
|
+
// than restated in the manifest.
|
|
1864
|
+
//
|
|
1865
|
+
// This used to union three manifest fields: `xema.dependencies` (server
|
|
1866
|
+
// biomes), `xema.requires` (an object form NO manifest has ever declared and
|
|
1867
|
+
// the strict schema now rejects — dead, and deleted with this change), and
|
|
1868
|
+
// `xema.requiresServerBiomes` (web biomes). That last one was never authored:
|
|
1869
|
+
// it was a generator-owned projection of "package dependencies ∩ generated
|
|
1870
|
+
// api-clients whose owner is a server biome". A web biome carries no
|
|
1871
|
+
// `xema.dependencies`, so it was the ONLY producer of `requires` for a web
|
|
1872
|
+
// biome — reading it after the field was deleted would silently produce
|
|
1873
|
+
// `requires: []`, which makes `requiresServerBiomesSatisfied` permanently
|
|
1874
|
+
// true and kills orphan detection outright.
|
|
1875
|
+
//
|
|
1876
|
+
// `requiredBiomeDependencyIds` answers the same question for BOTH targets
|
|
1877
|
+
// from one typed, evidence-carrying edge set: `xema.dependencies` for server
|
|
1878
|
+
// biomes and the derived client edges for web ones, filtered to
|
|
1879
|
+
// kind=biome + strength=required (service/connector/capability/host-abi edges
|
|
1880
|
+
// and optional edges are excluded by construction). Parsing with the kernel
|
|
1881
|
+
// schema first means a malformed or foreign index fails here rather than
|
|
1882
|
+
// resolving to an empty list.
|
|
1883
|
+
const kernelBiome = loadKernelBiomeContracts();
|
|
1884
|
+
let parsedIndex;
|
|
1885
|
+
try {
|
|
1886
|
+
parsedIndex = kernelBiome.BiomeBundleGeneratedIndexSchema.parse(generatedIndex);
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`${rel}: the committed ${BIOME_GENERATED_INDEX_FILENAME} is not a valid generated index: ${error.message}`,
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
if (parsedIndex.biome.biomeId !== id) {
|
|
1893
|
+
throw new Error(
|
|
1894
|
+
`${rel}: ${BIOME_GENERATED_INDEX_FILENAME} declares biomeId "${parsedIndex.biome.biomeId}" but the ` +
|
|
1895
|
+
`manifest declares "${id}" — the index does not describe this biome`,
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
if (parsedIndex.biome.version !== version) {
|
|
1899
|
+
throw new Error(
|
|
1900
|
+
`${rel}: ${BIOME_GENERATED_INDEX_FILENAME} declares version "${parsedIndex.biome.version}" but the ` +
|
|
1901
|
+
`manifest declares "${version}" — the index is stale; run \`pnpm derive:biome-index:write\``,
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
// Already de-duplicated and sorted by the kernel helper.
|
|
1905
|
+
const requiresUnique = [...kernelBiome.requiredBiomeDependencyIds(parsedIndex.biome)];
|
|
1906
|
+
|
|
1907
|
+
// Optional server biomes a web biome can integrate with but does not require
|
|
1908
|
+
// (never cause an ORPHANED resolution). Carried separately from `requires`.
|
|
1909
|
+
const optionalServerBiomes = Array.isArray(xema.optionalServerBiomes)
|
|
1910
|
+
? [...new Set(xema.optionalServerBiomes)].sort()
|
|
1911
|
+
: [];
|
|
1912
|
+
|
|
1913
|
+
// Capability DOMAINS this biome claims to own (manifest
|
|
1914
|
+
// `xema.ownsCapabilityDomains`). The claim is carried verbatim into the index
|
|
1915
|
+
// and then into the lock, where — and ONLY where — it becomes trustworthy:
|
|
1916
|
+
// being present in a RESOLVED, COMMITTED, drift-checked lock makes ownership a
|
|
1917
|
+
// distribution-author decision rather than a biome's self-declaration. See
|
|
1918
|
+
// `first-party-capability-domains.ts` in @xemahq/contracts.
|
|
1919
|
+
//
|
|
1920
|
+
// The claim is EXPLICIT — it never defaults to `capabilityDomain` or `id`. An
|
|
1921
|
+
// implicit default would silently make every locked biome first-party in its
|
|
1922
|
+
// own id-domain, which would newly REFUSE org provider bindings that are
|
|
1923
|
+
// legitimately org-bound today (e.g. `media:transcribe@1`).
|
|
1924
|
+
const ownsCapabilityDomains = Array.isArray(xema.ownsCapabilityDomains)
|
|
1925
|
+
? [...new Set(xema.ownsCapabilityDomains)].sort(compareStrings)
|
|
1926
|
+
: [];
|
|
1927
|
+
for (const domain of ownsCapabilityDomains) {
|
|
1928
|
+
if (typeof domain !== 'string' || !/^[a-z][a-z0-9-]*$/.test(domain)) {
|
|
1929
|
+
throw new Error(
|
|
1930
|
+
`${rel}: xema.ownsCapabilityDomains contains ${JSON.stringify(domain)}, ` +
|
|
1931
|
+
`which is not a valid capability domain (^[a-z][a-z0-9-]*$)`,
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
// A biome that declares a projection domain MUST also own it — otherwise it
|
|
1936
|
+
// projects refs into a domain any org could shadow with a provider binding.
|
|
1937
|
+
if (
|
|
1938
|
+
typeof xema.capabilityDomain === 'string' &&
|
|
1939
|
+
xema.capabilityDomain.length > 0 &&
|
|
1940
|
+
!ownsCapabilityDomains.includes(xema.capabilityDomain)
|
|
1941
|
+
) {
|
|
1942
|
+
throw new Error(
|
|
1943
|
+
`${rel}: xema.capabilityDomain "${xema.capabilityDomain}" is not listed in ` +
|
|
1944
|
+
`xema.ownsCapabilityDomains [${ownsCapabilityDomains.join(', ')}] — a biome that ` +
|
|
1945
|
+
`projects capabilities into a domain must also claim ownership of it.`,
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// --- Catalog projection (consumed by biome-host's DistributionCatalogSource) ---
|
|
1950
|
+
// The index/lock is the single source of truth for the biome CATALOG (which
|
|
1951
|
+
// biomes exist + the facts the web/server registry DTOs need), replacing the
|
|
1952
|
+
// filesystem-scan + vendored-manifest path. We carry the RAW manifest fields
|
|
1953
|
+
// the catalog derivation needs; biome-host applies the SAME `@xemahq/biome-sdk`
|
|
1954
|
+
// helpers (isSystemSurface / isBiomeStoreListed / defaultEnabledFor) it already
|
|
1955
|
+
// uses, so the derivation logic lives in exactly one place (no reimplementation
|
|
1956
|
+
// here, no drift).
|
|
1957
|
+
const displayName =
|
|
1958
|
+
typeof xema.displayName === 'string' && xema.displayName.length > 0
|
|
1959
|
+
? xema.displayName
|
|
1960
|
+
: id;
|
|
1961
|
+
// npm/workspace package name. For WEB biomes this is the SPA's import specifier
|
|
1962
|
+
// (load-bearing) — required. For server biomes it is cosmetic.
|
|
1963
|
+
const packageName = typeof raw.name === 'string' ? raw.name : null;
|
|
1964
|
+
if (target === 'web' && !packageName) {
|
|
1965
|
+
throw new Error(
|
|
1966
|
+
`${rel}: web biome is missing a top-level "name" (the SPA import specifier).`,
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
// Raw `xema.display` block (icon/category/summary/accent), passed through
|
|
1970
|
+
// verbatim; absent → omitted.
|
|
1971
|
+
const display =
|
|
1972
|
+
xema.display && typeof xema.display === 'object' ? xema.display : undefined;
|
|
1973
|
+
// Raw `xema.systemSurface` flag (renders in the org rail vs the store). Only a
|
|
1974
|
+
// boolean is meaningful; absence is normal and handled by the sdk helper.
|
|
1975
|
+
const systemSurface =
|
|
1976
|
+
typeof xema.systemSurface === 'boolean' ? xema.systemSurface : undefined;
|
|
1977
|
+
// Content-addressed source-manifest hash — lets biome-host (and diagnostics)
|
|
1978
|
+
// detect when a biome's authored manifest changed, distinct from distribution
|
|
1979
|
+
// resolution. Hash of the canonical manifest JSON.
|
|
1980
|
+
const { computeBiomeSourceManifestHash } = await loadKernelDistributionSchemas();
|
|
1981
|
+
const sourceManifestHash = await computeBiomeSourceManifestHash(raw);
|
|
1982
|
+
|
|
1983
|
+
// --- Installation class (the `{ installationClass }` selector's input) -----
|
|
1984
|
+
// HOW ESSENTIAL this biome is, folded from its components' declarations by
|
|
1985
|
+
// the KERNEL's `foldInstallationClass` — most essential wins. The fold is not
|
|
1986
|
+
// re-implemented here for the same reason `storeListed` calls
|
|
1987
|
+
// `isBiomeStoreListed`: a second implementation of a kernel rule is a second
|
|
1988
|
+
// answer waiting to drift, and this one decides whether a biome is on an
|
|
1989
|
+
// edition's FLOOR.
|
|
1990
|
+
//
|
|
1991
|
+
// The class is declared per COMPONENT and selected per BIOME because the
|
|
1992
|
+
// biome is the unit a distribution installs. Measured on 2026-09-05 across
|
|
1993
|
+
// all 34 classified manifests in repos/xema-base: NO biome mixes classes
|
|
1994
|
+
// across its runnable components, so today the fold is exact rather than
|
|
1995
|
+
// lossy. It is still a fold, and the kernel's docblock says which direction
|
|
1996
|
+
// it takes and why.
|
|
1997
|
+
//
|
|
1998
|
+
// OMITTED when no component declares a class — the third answer, never a
|
|
1999
|
+
// default. That also keeps the index byte-stable for every biome that
|
|
2000
|
+
// declares nothing, which at `origin/main` is currently all of them.
|
|
2001
|
+
const installationClass = kernelBiome.foldInstallationClass(
|
|
2002
|
+
components.map((component) =>
|
|
2003
|
+
component !== null && typeof component === 'object'
|
|
2004
|
+
? component.installationClass
|
|
2005
|
+
: undefined,
|
|
2006
|
+
),
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
return {
|
|
2010
|
+
id,
|
|
2011
|
+
tier,
|
|
2012
|
+
target,
|
|
2013
|
+
origin,
|
|
2014
|
+
version,
|
|
2015
|
+
audience,
|
|
2016
|
+
storeListed,
|
|
2017
|
+
// The kernel fold of the components' own declarations; OMITTED when none
|
|
2018
|
+
// declares one (see above), so an unclassed biome stays byte-stable.
|
|
2019
|
+
...(installationClass !== undefined ? { installationClass } : {}),
|
|
2020
|
+
services,
|
|
2021
|
+
// Omitted when empty — which is almost every biome — so the index stays
|
|
2022
|
+
// byte-stable for everything that needs no declaration.
|
|
2023
|
+
...(Object.keys(serviceDatabases).length > 0
|
|
2024
|
+
? {
|
|
2025
|
+
serviceDatabases: Object.fromEntries(
|
|
2026
|
+
Object.entries(serviceDatabases).sort(([a], [b]) =>
|
|
2027
|
+
compareStrings(a, b),
|
|
2028
|
+
),
|
|
2029
|
+
),
|
|
2030
|
+
}
|
|
2031
|
+
: {}),
|
|
2032
|
+
// Same omission rule as `serviceDatabases`, for the same reason: at
|
|
2033
|
+
// `origin/main` no manifest declares an extension, so an unconditional key
|
|
2034
|
+
// would move the bytes of every one of the ~117 entries for a fact none of
|
|
2035
|
+
// them carries.
|
|
2036
|
+
...(Object.keys(serviceDatabaseExtensions).length > 0
|
|
2037
|
+
? {
|
|
2038
|
+
serviceDatabaseExtensions: Object.fromEntries(
|
|
2039
|
+
Object.entries(serviceDatabaseExtensions).sort(([a], [b]) =>
|
|
2040
|
+
compareStrings(a, b),
|
|
2041
|
+
),
|
|
2042
|
+
),
|
|
2043
|
+
}
|
|
2044
|
+
: {}),
|
|
2045
|
+
runtimeComponents,
|
|
2046
|
+
hostModules,
|
|
2047
|
+
// Omitted when the biome declares none, or when its host modules already
|
|
2048
|
+
// carry it — see the projection above. At `origin/develop` that is every
|
|
2049
|
+
// committed entry, which is what keeps `biome-index.json` byte-stable.
|
|
2050
|
+
...(bundleSource !== undefined ? { bundleSource } : {}),
|
|
2051
|
+
// The biome's authorization module (plan biome-declared-authorization.md
|
|
2052
|
+
// §2.1): the author's SOURCE manifest `{ envelope, moduleFiles, testFile }`
|
|
2053
|
+
// and the `signedBiome` facts the sealer STAMPS onto it with the kernel's
|
|
2054
|
+
// `stampAuthorizationModuleContribution` — derived here, from this pinned
|
|
2055
|
+
// manifest and its generated index, never from the author's envelope.
|
|
2056
|
+
// Omitted when the biome ships none — every committed biome today — so
|
|
2057
|
+
// `biome-index.json` stays byte-stable.
|
|
2058
|
+
...(authorizationModule !== undefined
|
|
2059
|
+
? {
|
|
2060
|
+
authorizationModule: {
|
|
2061
|
+
source: authorizationModule,
|
|
2062
|
+
signedBiome: {
|
|
2063
|
+
tier,
|
|
2064
|
+
declaredBiomeDependencies: requiresUnique,
|
|
2065
|
+
serviceNames: [
|
|
2066
|
+
...new Set(
|
|
2067
|
+
(Array.isArray(xema.components) ? xema.components : [])
|
|
2068
|
+
.map((component) => component?.protocol?.serviceName)
|
|
2069
|
+
.filter((name) => typeof name === 'string' && name.length > 0),
|
|
2070
|
+
),
|
|
2071
|
+
].sort(compareStrings),
|
|
2072
|
+
},
|
|
2073
|
+
},
|
|
2074
|
+
}
|
|
2075
|
+
: {}),
|
|
2076
|
+
serviceRequirements,
|
|
2077
|
+
// Omitted when empty — which is every biome today — so the index stays
|
|
2078
|
+
// byte-stable for everything that declares nothing.
|
|
2079
|
+
...(serviceResources.length > 0 ? { serviceResources } : {}),
|
|
2080
|
+
...(providesResources.length > 0 ? { providesResources } : {}),
|
|
2081
|
+
// Omitted when empty — every committed biome today — so the index stays
|
|
2082
|
+
// byte-stable for everything that declares no Nexus service.
|
|
2083
|
+
...(nexusProvides.length > 0 ? { nexusProvides } : {}),
|
|
2084
|
+
...(nexusRequires.length > 0 ? { nexusRequires } : {}),
|
|
2085
|
+
// The v2 workers' task queues; omitted when the biome ships none (every
|
|
2086
|
+
// committed biome today), so the index stays byte-stable.
|
|
2087
|
+
...(Object.keys(workerTaskQueues).length > 0
|
|
2088
|
+
? {
|
|
2089
|
+
workerTaskQueues: Object.fromEntries(
|
|
2090
|
+
Object.entries(workerTaskQueues).sort(([a], [b]) => compareStrings(a, b)),
|
|
2091
|
+
),
|
|
2092
|
+
}
|
|
2093
|
+
: {}),
|
|
2094
|
+
// The queues each http service starts workflows on; omitted when no
|
|
2095
|
+
// component declares any, so the index stays byte-stable.
|
|
2096
|
+
...(Object.keys(workflowClients).length > 0
|
|
2097
|
+
? {
|
|
2098
|
+
workflowClients: Object.fromEntries(
|
|
2099
|
+
Object.entries(workflowClients).sort(([a], [b]) => compareStrings(a, b)),
|
|
2100
|
+
),
|
|
2101
|
+
}
|
|
2102
|
+
: {}),
|
|
2103
|
+
// The workers that run a sibling service's image with their own entry
|
|
2104
|
+
// module; omitted when the biome has none, so the index stays byte-stable.
|
|
2105
|
+
...(Object.keys(workerImages).length > 0
|
|
2106
|
+
? {
|
|
2107
|
+
workerImages: Object.fromEntries(
|
|
2108
|
+
Object.entries(workerImages).sort(([a], [b]) => compareStrings(a, b)),
|
|
2109
|
+
),
|
|
2110
|
+
}
|
|
2111
|
+
: {}),
|
|
2112
|
+
// Every platform-scheduled image this biome ships, with the facts a
|
|
2113
|
+
// composed process is graded on. Present for EVERY service of a
|
|
2114
|
+
// component-contract biome — a service declaring nothing still gets its
|
|
2115
|
+
// derived `componentKind` + `scalingMode`, because absent facts are what
|
|
2116
|
+
// the kernel grades as `unknown-member`. Omitted only when the biome ships
|
|
2117
|
+
// no platform-scheduled image (and for the retired `ships.apis[]` shape,
|
|
2118
|
+
// which declares no scaling class to read).
|
|
2119
|
+
...(Object.keys(serviceProcessFacts).length > 0
|
|
2120
|
+
? {
|
|
2121
|
+
serviceProcessFacts: Object.fromEntries(
|
|
2122
|
+
Object.entries(serviceProcessFacts).sort(([a], [b]) =>
|
|
2123
|
+
compareStrings(a, b),
|
|
2124
|
+
),
|
|
2125
|
+
),
|
|
2126
|
+
}
|
|
2127
|
+
: {}),
|
|
2128
|
+
// The biome's signed dependency contract — DERIVED by the builder from the
|
|
2129
|
+
// SDK seams its code calls, read from the committed generated index and
|
|
2130
|
+
// validated there by the kernel schema. The edge set positive closure
|
|
2131
|
+
// closes over. Omitted when the biome has not adopted one, which is every
|
|
2132
|
+
// committed biome today, so `biome-index.json` stays byte-stable; a
|
|
2133
|
+
// positive-closure edition REFUSES a root or required biome that lacks it.
|
|
2134
|
+
...(parsedIndex.biome.dependencyContract === undefined
|
|
2135
|
+
? {}
|
|
2136
|
+
: { dependencyContract: parsedIndex.biome.dependencyContract }),
|
|
2137
|
+
repo,
|
|
2138
|
+
path,
|
|
2139
|
+
requires: requiresUnique,
|
|
2140
|
+
// --- catalog projection ---
|
|
2141
|
+
// Omitted entirely when the biome claims nothing, so the index stays stable
|
|
2142
|
+
// for the overwhelming majority of biomes that own no domain.
|
|
2143
|
+
...(ownsCapabilityDomains.length > 0 ? { ownsCapabilityDomains } : {}),
|
|
2144
|
+
displayName,
|
|
2145
|
+
packageName,
|
|
2146
|
+
optionalServerBiomes,
|
|
2147
|
+
...(display ? { display } : {}),
|
|
2148
|
+
...(systemSurface !== undefined ? { systemSurface } : {}),
|
|
2149
|
+
sourceManifestHash,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/**
|
|
2154
|
+
* Build the full biome index (array of entries) by walking every manifest.
|
|
2155
|
+
* Sorted deterministically by (tier order, id, target, repo).
|
|
2156
|
+
*/
|
|
2157
|
+
function buildServiceOwners(entries, platformServiceIndex) {
|
|
2158
|
+
const serviceOwners = new Map();
|
|
2159
|
+
const registerOwner = (serviceName, owner, sourceDescription) => {
|
|
2160
|
+
const existing = serviceOwners.get(serviceName);
|
|
2161
|
+
if (existing !== undefined) {
|
|
2162
|
+
throw new Error(
|
|
2163
|
+
`service "${serviceName}" has ambiguous owners: ${existing.sourceDescription} and ${sourceDescription}`,
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
serviceOwners.set(serviceName, { owner, sourceDescription });
|
|
2167
|
+
};
|
|
2168
|
+
for (const entry of entries) {
|
|
2169
|
+
for (const serviceName of entry.services) {
|
|
2170
|
+
registerOwner(
|
|
2171
|
+
serviceName,
|
|
2172
|
+
{
|
|
2173
|
+
kind: 'biome',
|
|
2174
|
+
biomeId: entry.id,
|
|
2175
|
+
target: entry.target,
|
|
2176
|
+
repo: entry.repo,
|
|
2177
|
+
path: entry.path,
|
|
2178
|
+
version: entry.version,
|
|
2179
|
+
sourceManifestHash: entry.sourceManifestHash,
|
|
2180
|
+
},
|
|
2181
|
+
`biome "${entry.id}" (${entry.target}@${entry.repo}/${entry.path})`,
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
for (const source of platformServiceIndex.services) {
|
|
2186
|
+
registerOwner(
|
|
2187
|
+
source.name,
|
|
2188
|
+
{ kind: 'platform', source },
|
|
2189
|
+
`platform service (${source.sourceCommit}@${source.repo}/${source.path})`,
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
return serviceOwners;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
export function resolveServiceDependencyOwners(entries, platformServiceIndex) {
|
|
2196
|
+
const serviceOwners = buildServiceOwners(entries, platformServiceIndex);
|
|
2197
|
+
for (const entry of entries) {
|
|
2198
|
+
const serviceDependencies = [];
|
|
2199
|
+
for (const requirement of entry.serviceRequirements) {
|
|
2200
|
+
for (const requiredService of requirement.requiredServices) {
|
|
2201
|
+
const resolved = serviceOwners.get(requiredService);
|
|
2202
|
+
if (!resolved) {
|
|
2203
|
+
throw new Error(
|
|
2204
|
+
`biome "${entry.id}" API "${requirement.consumerService}" requires unknown service "${requiredService}"`,
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
if (
|
|
2208
|
+
resolved.owner.kind === 'biome' &&
|
|
2209
|
+
resolved.owner.target !== 'server'
|
|
2210
|
+
) {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
`biome "${entry.id}" API "${requirement.consumerService}" requires service "${requiredService}" ` +
|
|
2213
|
+
`owned by cross-target ${resolved.owner.target} biome "${resolved.owner.biomeId}"`,
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
serviceDependencies.push({
|
|
2217
|
+
consumerService: requirement.consumerService,
|
|
2218
|
+
requiredService,
|
|
2219
|
+
owner: resolved.owner,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
serviceDependencies.sort((a, b) => {
|
|
2224
|
+
const consumer = compareStrings(a.consumerService, b.consumerService);
|
|
2225
|
+
return consumer === 0
|
|
2226
|
+
? compareStrings(a.requiredService, b.requiredService)
|
|
2227
|
+
: consumer;
|
|
2228
|
+
});
|
|
2229
|
+
entry.serviceDependencies = serviceDependencies;
|
|
2230
|
+
delete entry.serviceRequirements;
|
|
2231
|
+
}
|
|
2232
|
+
return entries;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
/**
|
|
2236
|
+
* Offline parity guard for the committed index: every recorded edge must still
|
|
2237
|
+
* name the one canonical owner identity carried by that same index plus the
|
|
2238
|
+
* committed platform-service index.
|
|
2239
|
+
*/
|
|
2240
|
+
export function validateServiceDependencyOwnerParity(
|
|
2241
|
+
entries,
|
|
2242
|
+
platformServiceIndex,
|
|
2243
|
+
) {
|
|
2244
|
+
const serviceOwners = buildServiceOwners(entries, platformServiceIndex);
|
|
2245
|
+
for (const entry of entries) {
|
|
2246
|
+
if (!Array.isArray(entry.serviceDependencies)) {
|
|
2247
|
+
throw new Error(
|
|
2248
|
+
`biome "${entry.id}" has no serviceDependencies projection`,
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
for (const edge of entry.serviceDependencies) {
|
|
2252
|
+
const canonical = serviceOwners.get(edge.requiredService);
|
|
2253
|
+
if (!canonical) {
|
|
2254
|
+
throw new Error(
|
|
2255
|
+
`biome "${entry.id}" API "${edge.consumerService}" requires unknown service "${edge.requiredService}"`,
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
if (
|
|
2259
|
+
canonical.owner.kind === 'biome' &&
|
|
2260
|
+
canonical.owner.target !== 'server'
|
|
2261
|
+
) {
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
`biome "${entry.id}" API "${edge.consumerService}" requires cross-target service "${edge.requiredService}"`,
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
if (serializeJson(edge.owner) !== serializeJson(canonical.owner)) {
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
`biome "${entry.id}" API "${edge.consumerService}" service "${edge.requiredService}" ` +
|
|
2269
|
+
`owner identity does not match ${canonical.sourceDescription}`,
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
return entries;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
export async function buildBiomeIndex(
|
|
2278
|
+
platformServiceIndex,
|
|
2279
|
+
sourceRepositories = distributionBiomeSourceRepositories(),
|
|
2280
|
+
sourceAuthority,
|
|
2281
|
+
) {
|
|
2282
|
+
const authority =
|
|
2283
|
+
sourceAuthority ??
|
|
2284
|
+
createSourceAuthority({
|
|
2285
|
+
knownRepositories: sourceRepositories.map(({ repo }) => repo),
|
|
2286
|
+
});
|
|
2287
|
+
const entries = [];
|
|
2288
|
+
for (const manifest of await committedBiomeManifests(sourceRepositories, authority)) {
|
|
2289
|
+
entries.push(await manifestToIndexEntry(manifest));
|
|
2290
|
+
}
|
|
2291
|
+
resolveServiceDependencyOwners(entries, platformServiceIndex);
|
|
2292
|
+
assertDeclaredDatabasesUnowned(entries);
|
|
2293
|
+
entries.sort(compareIndexEntries);
|
|
2294
|
+
return entries;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* A declared `serviceDatabases` key must name a database NOTHING else in the
|
|
2299
|
+
* platform already answers to.
|
|
2300
|
+
*
|
|
2301
|
+
* This is the bound on the entitlement. org-database-pool-api hands out the
|
|
2302
|
+
* shared cluster's credential for whatever key the caller is entitled to, and
|
|
2303
|
+
* the two derived entitlements (biome id, service name minus `-api`) are
|
|
2304
|
+
* self-evidently non-transferable because they are derivations of the caller's
|
|
2305
|
+
* own verified identity. A DECLARED key is not — it is a free string in a
|
|
2306
|
+
* manifest — so the only thing that keeps it from naming `llm-registry` is
|
|
2307
|
+
* this check. The pool re-asserts it when it loads the lock; failing here too
|
|
2308
|
+
* means a manifest that would strand or hijack a database fails the build
|
|
2309
|
+
* instead of the fleet.
|
|
2310
|
+
*/
|
|
2311
|
+
export function assertDeclaredDatabasesUnowned(entries) {
|
|
2312
|
+
const claimedBy = new Map();
|
|
2313
|
+
for (const entry of entries) {
|
|
2314
|
+
claimedBy.set(entry.id, `biome "${entry.id}"`);
|
|
2315
|
+
}
|
|
2316
|
+
for (const entry of entries) {
|
|
2317
|
+
for (const service of entry.services ?? []) {
|
|
2318
|
+
claimedBy.set(service.replace(/-api$/, ''), `service "${service}"`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
for (const entry of entries) {
|
|
2322
|
+
for (const [service, key] of Object.entries(entry.serviceDatabases ?? {})) {
|
|
2323
|
+
if (key === service.replace(/-api$/, '') || key === entry.id) {
|
|
2324
|
+
// Already reachable by derivation; the declaration widens nothing.
|
|
2325
|
+
continue;
|
|
2326
|
+
}
|
|
2327
|
+
const claimant = claimedBy.get(key);
|
|
2328
|
+
if (claimant !== undefined) {
|
|
2329
|
+
throw new Error(
|
|
2330
|
+
`biome "${entry.id}" declares database "${key}" for service ` +
|
|
2331
|
+
`"${service}", but "${key}" is already the database of ${claimant}. ` +
|
|
2332
|
+
`A declared database must name one no other biome or service owns — ` +
|
|
2333
|
+
`otherwise the declaration hands "${service}" another principal's schema.`,
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
claimedBy.set(key, `service "${service}"`);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Build the `capabilityDomainOwners` reservation table from the FULL biome
|
|
2343
|
+
* index — every biome the packaging pipeline discovered, NOT the subset an
|
|
2344
|
+
* edition selects.
|
|
2345
|
+
*
|
|
2346
|
+
* WHY THE FULL INDEX (this is the whole point of the table):
|
|
2347
|
+
*
|
|
2348
|
+
* Ownership is a property of the PLATFORM, not of what an edition happens to
|
|
2349
|
+
* preload. If the table were built from the selected set, a trimmed edition
|
|
2350
|
+
* (`minimal`, `appliance-lean`) that ships without `knowledge-base` would
|
|
2351
|
+
* leave `kb` unowned — and an org admin could then install `knowledge-base`
|
|
2352
|
+
* from the store and bind `kb:page.read@1` to a URL they control, silently
|
|
2353
|
+
* shadowing the platform data plane. Being AUTHORED by a first-party biome is
|
|
2354
|
+
* what makes a domain un-shadowable; whether a given edition preloads that
|
|
2355
|
+
* biome is a packaging choice and must never change the security answer.
|
|
2356
|
+
*
|
|
2357
|
+
* So every edition gets the IDENTICAL table. A domain is reserved even when
|
|
2358
|
+
* its owning biome is absent from the lock — that is precisely the case the
|
|
2359
|
+
* reservation exists to cover.
|
|
2360
|
+
*
|
|
2361
|
+
* Sorted by `domain` for deterministic, diffable output.
|
|
2362
|
+
*
|
|
2363
|
+
* @throws when two biomes claim the same domain. Ambiguous ownership decides
|
|
2364
|
+
* whether an org may bind refs under the domain, so it has no safe resolution
|
|
2365
|
+
* and must fail the build rather than pick a winner.
|
|
2366
|
+
*/
|
|
2367
|
+
export function buildCapabilityDomainOwners(index) {
|
|
2368
|
+
const owners = new Map();
|
|
2369
|
+
for (const entry of index) {
|
|
2370
|
+
for (const domain of entry.ownsCapabilityDomains ?? []) {
|
|
2371
|
+
const incumbent = owners.get(domain);
|
|
2372
|
+
if (incumbent !== undefined && incumbent !== entry.id) {
|
|
2373
|
+
throw new Error(
|
|
2374
|
+
`capability domain "${domain}" is claimed by BOTH biome "${incumbent}" and biome "${entry.id}". ` +
|
|
2375
|
+
`A domain has exactly one owner — ownership decides whether an org may bind refs in it, ` +
|
|
2376
|
+
`so an ambiguous claim is refused.`,
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
owners.set(domain, entry.id);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return [...owners.entries()]
|
|
2383
|
+
.map(([domain, biomeId]) => ({ domain, biomeId }))
|
|
2384
|
+
.sort((a, b) => compareStrings(a.domain, b.domain));
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/** Deterministic ordering used for both the index and the lock. */
|
|
2388
|
+
export function compareIndexEntries(a, b) {
|
|
2389
|
+
const order = biomeTierOrder();
|
|
2390
|
+
const ta = order.indexOf(a.tier);
|
|
2391
|
+
const tb = order.indexOf(b.tier);
|
|
2392
|
+
if (ta !== tb) {return ta - tb;}
|
|
2393
|
+
if (a.id !== b.id) {return a.id < b.id ? -1 : 1;}
|
|
2394
|
+
if (a.target !== b.target) {return a.target < b.target ? -1 : 1;}
|
|
2395
|
+
if (a.repo !== b.repo) {return a.repo < b.repo ? -1 : 1;}
|
|
2396
|
+
return 0;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/** Stable JSON serialization (pretty, trailing newline) for committed files. */
|
|
2400
|
+
export function serializeJson(value) {
|
|
2401
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Shared generated-artifact currency gate. Callers supply canonical serialized
|
|
2406
|
+
* bytes; any authority change must make a previously committed projection fail.
|
|
2407
|
+
*/
|
|
2408
|
+
export function assertGeneratedArtifactCurrent(
|
|
2409
|
+
label,
|
|
2410
|
+
committed,
|
|
2411
|
+
generated,
|
|
2412
|
+
regenerateCommand,
|
|
2413
|
+
) {
|
|
2414
|
+
if (committed !== generated) {
|
|
2415
|
+
throw new Error(
|
|
2416
|
+
`${label} is out of date. Run \`${regenerateCommand}\`.`,
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
/** sha256 hex of a string. */
|
|
2422
|
+
export function sha256Hex(input) {
|
|
2423
|
+
return createHash('sha256').update(input).digest('hex');
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
const EXACT_SEMVER_PATTERN =
|
|
2427
|
+
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
2428
|
+
const IMAGE_REPOSITORY_SEGMENT_PATTERN =
|
|
2429
|
+
/^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*$/;
|
|
2430
|
+
const MAX_REGISTRY_HOST_LENGTH = 253;
|
|
2431
|
+
const MAX_DNS_LABEL_LENGTH = 63;
|
|
2432
|
+
const MAX_REPOSITORY_PATH_LENGTH = 255;
|
|
2433
|
+
|
|
2434
|
+
function isCanonicalRegistryPort(port) {
|
|
2435
|
+
if (port === undefined) {return true;}
|
|
2436
|
+
if (!/^[0-9]+$/.test(port)) {return false;}
|
|
2437
|
+
const numericPort = Number(port);
|
|
2438
|
+
return numericPort >= 1 && numericPort <= 65535;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function isCanonicalDnsHost(host) {
|
|
2442
|
+
if (
|
|
2443
|
+
host.length === 0 ||
|
|
2444
|
+
host.length > MAX_REGISTRY_HOST_LENGTH ||
|
|
2445
|
+
host !== host.toLowerCase() ||
|
|
2446
|
+
host.endsWith('.')
|
|
2447
|
+
) {
|
|
2448
|
+
return false;
|
|
2449
|
+
}
|
|
2450
|
+
if (/^[0-9.]+$/.test(host)) {return isIP(host) === 4;}
|
|
2451
|
+
const labels = host.split('.');
|
|
2452
|
+
return labels.every(
|
|
2453
|
+
(label) =>
|
|
2454
|
+
label.length >= 1 &&
|
|
2455
|
+
label.length <= MAX_DNS_LABEL_LENGTH &&
|
|
2456
|
+
/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(label),
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
export function isCanonicalRegistryAuthority(authority) {
|
|
2461
|
+
if (authority.startsWith('[')) {
|
|
2462
|
+
const closingBracket = authority.indexOf(']');
|
|
2463
|
+
if (closingBracket < 0) {return false;}
|
|
2464
|
+
const address = authority.slice(1, closingBracket);
|
|
2465
|
+
const suffix = authority.slice(closingBracket + 1);
|
|
2466
|
+
if (
|
|
2467
|
+
address.length === 0 ||
|
|
2468
|
+
address !== address.toLowerCase() ||
|
|
2469
|
+
isIP(address) !== 6
|
|
2470
|
+
) {
|
|
2471
|
+
return false;
|
|
2472
|
+
}
|
|
2473
|
+
if (suffix.length === 0) {return true;}
|
|
2474
|
+
return suffix.startsWith(':') && isCanonicalRegistryPort(suffix.slice(1));
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
const firstColon = authority.indexOf(':');
|
|
2478
|
+
const lastColon = authority.lastIndexOf(':');
|
|
2479
|
+
if (firstColon !== lastColon) {return false;}
|
|
2480
|
+
const host = lastColon < 0 ? authority : authority.slice(0, lastColon);
|
|
2481
|
+
const port = lastColon < 0 ? undefined : authority.slice(lastColon + 1);
|
|
2482
|
+
return isCanonicalDnsHost(host) && isCanonicalRegistryPort(port);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function isUntaggedImageRepository(imageRef) {
|
|
2486
|
+
if (
|
|
2487
|
+
typeof imageRef !== 'string' ||
|
|
2488
|
+
imageRef.length === 0 ||
|
|
2489
|
+
imageRef.trim() !== imageRef ||
|
|
2490
|
+
imageRef.includes('@') ||
|
|
2491
|
+
imageRef.includes('://') ||
|
|
2492
|
+
imageRef.includes('//') ||
|
|
2493
|
+
imageRef.endsWith('/')
|
|
2494
|
+
) {
|
|
2495
|
+
return false;
|
|
2496
|
+
}
|
|
2497
|
+
const [authority, ...segments] = imageRef.split('/');
|
|
2498
|
+
const repositoryPath = segments.join('/');
|
|
2499
|
+
if (
|
|
2500
|
+
segments.length === 0 ||
|
|
2501
|
+
repositoryPath.length > MAX_REPOSITORY_PATH_LENGTH ||
|
|
2502
|
+
!isCanonicalRegistryAuthority(authority) ||
|
|
2503
|
+
segments.some(
|
|
2504
|
+
(segment) =>
|
|
2505
|
+
segment.includes(':') ||
|
|
2506
|
+
!IMAGE_REPOSITORY_SEGMENT_PATTERN.test(segment),
|
|
2507
|
+
)
|
|
2508
|
+
) {
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2511
|
+
return true;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
export const IMAGE_PRODUCER_EVIDENCE_PREREQUISITE =
|
|
2515
|
+
'a complete authorized signed image-producer evidence set that binds the ' +
|
|
2516
|
+
'exact distribution inputHash, source repository commit, sourceManifestHash, ' +
|
|
2517
|
+
'source-tree contentHash, image repository, OCI manifest digest, complete ' +
|
|
2518
|
+
'platform set, SBOM, provenance, distribution release, and customer entitlement';
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Enforce the immutable fields carried by the existing DistributionLock
|
|
2522
|
+
* schema. The kernel schema owns the shape; this packaging-plane guard owns
|
|
2523
|
+
* the stronger provenance invariant: a resolved lock may contain only exact
|
|
2524
|
+
* versions and full sha256 source-manifest identities.
|
|
2525
|
+
*/
|
|
2526
|
+
export function validateImmutableBiomeIndex(index) {
|
|
2527
|
+
for (const biome of index) {
|
|
2528
|
+
if (!EXACT_SEMVER_PATTERN.test(biome.version)) {
|
|
2529
|
+
throw new Error(
|
|
2530
|
+
`biome "${biome.id}" has mutable or invalid ` +
|
|
2531
|
+
`version "${biome.version}" — resolved locks require an exact semantic version`,
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
if (!SHA256_HEX_PATTERN.test(biome.sourceManifestHash)) {
|
|
2535
|
+
throw new Error(
|
|
2536
|
+
`biome "${biome.id}" has invalid ` +
|
|
2537
|
+
`sourceManifestHash "${String(biome.sourceManifestHash)}" — expected 64 lowercase sha256 hex characters`,
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
const services = new Set();
|
|
2541
|
+
for (const service of biome.services ?? []) {
|
|
2542
|
+
if (services.has(service)) {
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
`biome "${biome.id}" owns duplicate service "${service}"`,
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
services.add(service);
|
|
2548
|
+
}
|
|
2549
|
+
// Runtime components are built/signed/mirrored like services but never
|
|
2550
|
+
// rostered. They must be a disjoint set from `services` — a name in both
|
|
2551
|
+
// would be simultaneously scheduled and unschedulable.
|
|
2552
|
+
const runtimeComponents = new Set();
|
|
2553
|
+
for (const component of biome.runtimeComponents ?? []) {
|
|
2554
|
+
if (runtimeComponents.has(component)) {
|
|
2555
|
+
throw new Error(
|
|
2556
|
+
`biome "${biome.id}" owns duplicate runtime component "${component}"`,
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
if (services.has(component)) {
|
|
2560
|
+
throw new Error(
|
|
2561
|
+
`biome "${biome.id}" declares "${component}" both as a service and as a ` +
|
|
2562
|
+
`runtime component — an image is either scheduled or it is not`,
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
runtimeComponents.add(component);
|
|
2566
|
+
}
|
|
2567
|
+
const edges = new Set();
|
|
2568
|
+
for (const edge of biome.serviceDependencies ?? []) {
|
|
2569
|
+
if (!services.has(edge.consumerService)) {
|
|
2570
|
+
throw new Error(
|
|
2571
|
+
`biome "${biome.id}" service dependency names unknown consumer API "${edge.consumerService}"`,
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
const edgeKey = `${edge.consumerService}\0${edge.requiredService}`;
|
|
2575
|
+
if (edges.has(edgeKey)) {
|
|
2576
|
+
throw new Error(
|
|
2577
|
+
`biome "${biome.id}" has duplicate service dependency ${edge.consumerService} -> ${edge.requiredService}`,
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
edges.add(edgeKey);
|
|
2581
|
+
if (edge.owner?.kind === 'biome') {
|
|
2582
|
+
if (edge.owner.target !== 'server') {
|
|
2583
|
+
throw new Error(
|
|
2584
|
+
`biome "${biome.id}" service dependency ${edge.consumerService} -> ${edge.requiredService} ` +
|
|
2585
|
+
`has cross-target ${String(edge.owner.target)} owner`,
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
if (!EXACT_SEMVER_PATTERN.test(edge.owner.version)) {
|
|
2589
|
+
throw new Error(
|
|
2590
|
+
`biome "${biome.id}" service dependency owner "${edge.owner.biomeId}" has invalid exact version`,
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
if (!SHA256_HEX_PATTERN.test(edge.owner.sourceManifestHash)) {
|
|
2594
|
+
throw new Error(
|
|
2595
|
+
`biome "${biome.id}" service dependency owner "${edge.owner.biomeId}" has invalid sourceManifestHash`,
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
} else if (edge.owner?.kind === 'platform') {
|
|
2599
|
+
if (edge.owner.source?.name !== edge.requiredService) {
|
|
2600
|
+
throw new Error(
|
|
2601
|
+
`biome "${biome.id}" service dependency ${edge.consumerService} -> ${edge.requiredService} ` +
|
|
2602
|
+
`has mismatched platform owner "${String(edge.owner.source?.name)}"`,
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
} else {
|
|
2606
|
+
throw new Error(
|
|
2607
|
+
`biome "${biome.id}" service dependency ${edge.consumerService} -> ${edge.requiredService} has no recognized owner`,
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return index;
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
/**
|
|
2616
|
+
* The distribution lock as the resolve pipeline reads it: the UNSEALED
|
|
2617
|
+
* document a resolve just wrote, or the SEALED v5 lock a prior seal committed.
|
|
2618
|
+
* Both carry the same body; the kernel's two parsers each refuse the other
|
|
2619
|
+
* shape, so this dispatches on the document's own marker and never guesses.
|
|
2620
|
+
* A pre-v5 lock is refused by the kernel (`unsupported-schema-version`).
|
|
2621
|
+
*/
|
|
2622
|
+
export function parseResolvedDistributionLock(schemas, raw) {
|
|
2623
|
+
return raw !== null && typeof raw === 'object' && 'unsealedSchema' in raw
|
|
2624
|
+
? schemas.parseUnsealedDistributionLock(raw)
|
|
2625
|
+
: schemas.parseDistributionLockForReading(raw);
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
/**
|
|
2629
|
+
* The text two distribution locks are compared by: the UNSEALED projection of
|
|
2630
|
+
* each, with the resolve timestamp removed. A committed lock is sealed (v5) and
|
|
2631
|
+
* a fresh resolve is unsealed; the kernel's `unsealedProjection` maps the first
|
|
2632
|
+
* onto the shape of the second (its key order is stable, so the comparison is
|
|
2633
|
+
* byte for byte). Anything that is not one of the two v5 shapes is refused by
|
|
2634
|
+
* the kernel's parsers, a pre-v5 lock with `unsupported-schema-version`.
|
|
2635
|
+
*/
|
|
2636
|
+
export function comparableDistributionLock(schemas, raw) {
|
|
2637
|
+
const unsealed = schemas.parseUnsealedDistributionLock(
|
|
2638
|
+
raw !== null && typeof raw === 'object' && 'unsealedSchema' in raw
|
|
2639
|
+
? raw
|
|
2640
|
+
: schemas.unsealedProjection(schemas.parseDistributionLock(raw)),
|
|
2641
|
+
);
|
|
2642
|
+
const { resolvedAt: _resolvedAt, ...rest } = unsealed;
|
|
2643
|
+
return serializeJson(rest);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
export function validateImmutableDistributionLock(lock) {
|
|
2647
|
+
if (!SHA256_HEX_PATTERN.test(lock.inputHash)) {
|
|
2648
|
+
throw new Error(
|
|
2649
|
+
`distribution "${lock.distributionId}" has invalid inputHash "${String(lock.inputHash)}" — ` +
|
|
2650
|
+
'expected 64 lowercase sha256 hex characters',
|
|
2651
|
+
);
|
|
2652
|
+
}
|
|
2653
|
+
try {
|
|
2654
|
+
validateImmutableBiomeIndex(lock.biomes ?? []);
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
throw new Error(`distribution "${lock.distributionId}": ${error.message}`, {
|
|
2657
|
+
cause: error,
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
return lock;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/**
|
|
2664
|
+
* Enforce source-addressed image identities after canonical ImageLock parsing.
|
|
2665
|
+
* `imageRef` is deliberately an untagged repository; the immutable build tag
|
|
2666
|
+
* is the exact sha256 `contentHash` under the lock's explicit hash algorithm.
|
|
2667
|
+
*/
|
|
2668
|
+
export function validateImmutableImageLock(lock, schemas) {
|
|
2669
|
+
if (!schemas) {
|
|
2670
|
+
throw new Error('trusted kernel distribution schemas are required');
|
|
2671
|
+
}
|
|
2672
|
+
if (
|
|
2673
|
+
lock.schemaVersion !== 3 ||
|
|
2674
|
+
lock.sourceHashAlgorithm !== schemas.ImageSourceHashAlgorithm.ImageInputV2
|
|
2675
|
+
) {
|
|
2676
|
+
throw new Error(
|
|
2677
|
+
`distribution "${lock.distributionId}" has incompatible image-lock schema ${String(lock.schemaVersion)} ` +
|
|
2678
|
+
`and sourceHashAlgorithm "${String(lock.sourceHashAlgorithm)}"`,
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
const identities = new Set();
|
|
2682
|
+
for (const subject of lock.subjects ?? []) {
|
|
2683
|
+
const identity = `${subject.kind}\0${subject.id}`;
|
|
2684
|
+
if (identities.has(identity)) {
|
|
2685
|
+
throw new Error(
|
|
2686
|
+
`distribution "${lock.distributionId}" has duplicate runtime image subject "${subject.kind}:${subject.id}"`,
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
identities.add(identity);
|
|
2690
|
+
if (!isUntaggedImageRepository(subject.imageRef)) {
|
|
2691
|
+
throw new Error(
|
|
2692
|
+
`distribution "${lock.distributionId}": runtime subject "${subject.kind}:${subject.id}" has mutable or invalid ` +
|
|
2693
|
+
`imageRef "${subject.imageRef}" — expected an untagged image repository`,
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
if (!SHA256_HEX_PATTERN.test(subject.contentHash)) {
|
|
2697
|
+
throw new Error(
|
|
2698
|
+
`distribution "${lock.distributionId}": runtime subject "${subject.kind}:${subject.id}" has invalid ` +
|
|
2699
|
+
`contentHash "${String(subject.contentHash)}" — expected 64 lowercase sha256 hex characters`,
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2702
|
+
const owners = new Set();
|
|
2703
|
+
for (const owner of subject.owners ?? []) {
|
|
2704
|
+
const key = serializeJson(owner);
|
|
2705
|
+
if (owners.has(key)) {
|
|
2706
|
+
throw new Error(
|
|
2707
|
+
`distribution "${lock.distributionId}": runtime subject "${subject.kind}:${subject.id}" has a duplicate owner`,
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
owners.add(key);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
const platformServices = new Set();
|
|
2714
|
+
for (const binding of lock.platformServiceBindings ?? []) {
|
|
2715
|
+
if (platformServices.has(binding.platformService)) {
|
|
2716
|
+
throw new Error(
|
|
2717
|
+
`distribution "${lock.distributionId}" has duplicate runtime image binding for platform service "${binding.platformService}"`,
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
platformServices.add(binding.platformService);
|
|
2721
|
+
if (!identities.has(`${binding.subjectKind}\0${binding.subjectId}`)) {
|
|
2722
|
+
throw new Error(
|
|
2723
|
+
`distribution "${lock.distributionId}": platform service "${binding.platformService}" binds unknown runtime subject "${binding.subjectKind}:${binding.subjectId}"`,
|
|
2724
|
+
);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return lock;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
export function canonicalPublicHttpsRepositoryUrl(raw, context) {
|
|
2731
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.trim() !== raw) {
|
|
2732
|
+
throw new Error(`${context} must be a non-empty canonical HTTPS URL`);
|
|
2733
|
+
}
|
|
2734
|
+
let parsed;
|
|
2735
|
+
try {
|
|
2736
|
+
parsed = new URL(raw);
|
|
2737
|
+
} catch {
|
|
2738
|
+
throw new Error(`${context} is not a valid URL`);
|
|
2739
|
+
}
|
|
2740
|
+
if (parsed.protocol !== 'https:') {
|
|
2741
|
+
throw new Error(`${context} must use HTTPS`);
|
|
2742
|
+
}
|
|
2743
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
2744
|
+
throw new Error(
|
|
2745
|
+
`${context} must not contain username/password credentials`,
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
if (
|
|
2749
|
+
parsed.search.length > 0 ||
|
|
2750
|
+
parsed.hash.length > 0 ||
|
|
2751
|
+
raw.includes('?') ||
|
|
2752
|
+
raw.includes('#')
|
|
2753
|
+
) {
|
|
2754
|
+
throw new Error(`${context} must not contain a query or fragment`);
|
|
2755
|
+
}
|
|
2756
|
+
if (parsed.hostname.length === 0) {
|
|
2757
|
+
throw new Error(`${context} must identify a repository host`);
|
|
2758
|
+
}
|
|
2759
|
+
return parsed.toString();
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
/**
|
|
2763
|
+
* Assert that an exact 40-hex commit exists in a source repository. Delegates
|
|
2764
|
+
* to `@xemahq/distribution-source-hash` — the tooling holds no second copy of
|
|
2765
|
+
* the Git-object reader the content hash is built on.
|
|
2766
|
+
*/
|
|
2767
|
+
export function assertGitCommitExists(repoDir, commit) {
|
|
2768
|
+
return distributionSourceHash().assertGitCommitExists(repoDir, commit);
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/** Split the NUL-delimited output of a `-z` Git plumbing command. */
|
|
2772
|
+
function decodeNullDelimitedGitRecords(raw, context) {
|
|
2773
|
+
return distributionSourceHash().decodeNullDelimitedGitRecords(raw, context);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
/**
|
|
2777
|
+
* Hash the immutable tree of an exact Git commit — a distribution image
|
|
2778
|
+
* subject's `contentHash`, and therefore its published image tag.
|
|
2779
|
+
*/
|
|
2780
|
+
export function hashGitSourceTree(
|
|
2781
|
+
repoDir,
|
|
2782
|
+
repoRelativeSourcePath,
|
|
2783
|
+
commit,
|
|
2784
|
+
algorithm,
|
|
2785
|
+
) {
|
|
2786
|
+
return distributionSourceHash().hashGitSourceTree(
|
|
2787
|
+
repoDir,
|
|
2788
|
+
repoRelativeSourcePath,
|
|
2789
|
+
commit,
|
|
2790
|
+
algorithm,
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
/**
|
|
2795
|
+
* Every workspace package in a repository at an exact commit, keyed by name.
|
|
2796
|
+
*
|
|
2797
|
+
* Delegates to `@xemahq/distribution-source-hash`. The tooling holds no second
|
|
2798
|
+
* copy of the workspace-graph reader: it is derived by walking the commit's
|
|
2799
|
+
* tree for `package.json` files rather than by expanding `pnpm-workspace.yaml`
|
|
2800
|
+
* globs, and a hand-rolled YAML parser on the critical path of a release
|
|
2801
|
+
* coordinate is exactly what that package exists to avoid.
|
|
2802
|
+
*/
|
|
2803
|
+
export function workspacePackagesAtCommit(repoDir, commit, tree) {
|
|
2804
|
+
return distributionSourceHash().workspacePackagesAtCommit(repoDir, commit, tree);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
/**
|
|
2808
|
+
* The path index of one commit's tree — the input `workspacePackagesAtCommit`
|
|
2809
|
+
* scans for `package.json` files.
|
|
2810
|
+
*
|
|
2811
|
+
* Exposed so a caller can build it ONCE per repository and reuse it across
|
|
2812
|
+
* every biome in that repository. Building it per biome would re-read the whole
|
|
2813
|
+
* tree 123 times for one index.
|
|
2814
|
+
*/
|
|
2815
|
+
export function gitCommitTreeIndex(repoDir, commit) {
|
|
2816
|
+
return distributionSourceHash().GitCommitTreeIndex.atCommit(repoDir, commit);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
/**
|
|
2820
|
+
* The transitive workspace-package closure of one package directory, EXCLUDING
|
|
2821
|
+
* the root — the root is hashed separately as its own source tree, so
|
|
2822
|
+
* including it would frame one digest twice.
|
|
2823
|
+
*
|
|
2824
|
+
* Returns `[]` for a directory that is not a workspace member at all. For a
|
|
2825
|
+
* BIOME that is the correct answer and not a gap: a biome with no
|
|
2826
|
+
* `package.json` runs no build, so nothing outside its directory can change its
|
|
2827
|
+
* bundle's bytes. `check-biome-bundle-closure-derivable` pins the premise that
|
|
2828
|
+
* makes that true — a biome shipping BUILT OUTPUT always has a manifest.
|
|
2829
|
+
*/
|
|
2830
|
+
export function workspaceClosure(rootDirectory, graph, commit) {
|
|
2831
|
+
return distributionSourceHash().workspaceClosure(rootDirectory, graph, commit);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
/**
|
|
2835
|
+
* Length-framed hash update — the same framing the image content hash uses.
|
|
2836
|
+
*
|
|
2837
|
+
* Framing is not decoration: without it, two different component lists can
|
|
2838
|
+
* concatenate to identical bytes and collide onto one coordinate. Delegated
|
|
2839
|
+
* rather than re-implemented for that reason.
|
|
2840
|
+
*/
|
|
2841
|
+
export function updateFramedHash(hash, field, value) {
|
|
2842
|
+
return distributionSourceHash().updateFramedHash(hash, field, value);
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
/**
|
|
2846
|
+
* Hash the FULL build-input closure of one service image — the subject's
|
|
2847
|
+
* `contentHash`, and therefore its published image tag.
|
|
2848
|
+
*
|
|
2849
|
+
* Delegates to `@xemahq/distribution-source-hash`, which is the same code every
|
|
2850
|
+
* producing repository's CI runs to derive the tag it pushes. The tooling holds
|
|
2851
|
+
* no second copy: a divergence would resolve a lock whose images sit at a
|
|
2852
|
+
* coordinate nothing published.
|
|
2853
|
+
*/
|
|
2854
|
+
export function hashImageBuildInputs(request, algorithm) {
|
|
2855
|
+
return distributionSourceHash().hashImageBuildInputs(request, algorithm);
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
/**
|
|
2859
|
+
* Read + parse a JSON file out of an exact Git commit (never the work tree).
|
|
2860
|
+
*
|
|
2861
|
+
* This is the seam every authored source file crosses on its way into a lock
|
|
2862
|
+
* or an index — `xema-biome.json`, a platform service's `package.json`,
|
|
2863
|
+
* `cross-repo-build-inputs.json`, the committed generated biome index — so it
|
|
2864
|
+
* parses STRICTLY: a duplicated object key is refused, naming the commit, the
|
|
2865
|
+
* path and the JSON pointer of the object. `JSON.parse` would keep the last
|
|
2866
|
+
* occurrence, which is how a `resources` declaration was dropped from a sealed
|
|
2867
|
+
* lock with every reader agreeing (see `strict-json.mjs`).
|
|
2868
|
+
*
|
|
2869
|
+
* The commit check, the buffer ceiling and the exact-UTF-8 decode are the
|
|
2870
|
+
* source-hash package's own; only the parse differs from its
|
|
2871
|
+
* `readGitJsonAtCommit`, which is lenient and is therefore not delegated to.
|
|
2872
|
+
*/
|
|
2873
|
+
export function readGitJsonAtCommit(repoDir, repoRelativePath, commit) {
|
|
2874
|
+
const sourceHash = distributionSourceHash();
|
|
2875
|
+
sourceHash.assertGitCommitExists(repoDir, commit);
|
|
2876
|
+
let raw;
|
|
2877
|
+
try {
|
|
2878
|
+
raw = execFileSync(
|
|
2879
|
+
'git',
|
|
2880
|
+
['-C', repoDir, 'show', `${commit}:${repoRelativePath}`],
|
|
2881
|
+
{
|
|
2882
|
+
encoding: 'buffer',
|
|
2883
|
+
maxBuffer: sourceHash.GIT_OBJECT_MAX_BUFFER_BYTES,
|
|
2884
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2885
|
+
},
|
|
2886
|
+
);
|
|
2887
|
+
} catch {
|
|
2888
|
+
throw new Error(
|
|
2889
|
+
`cannot read "${repoRelativePath}" from Git commit "${commit}"`,
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
const context = `Git object "${commit}:${repoRelativePath}"`;
|
|
2893
|
+
return parseStrictJson(sourceHash.decodeExactUtf8(raw, context), context);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* The producing repository's OWN committed record of what it vendored.
|
|
2898
|
+
*
|
|
2899
|
+
* `vendor-cross-repo-inputs.mjs` reads exactly this file, at the commit being
|
|
2900
|
+
* built, to emit the `--vendored <dest>=<repoDir>@<commit>:<src>` arguments it
|
|
2901
|
+
* hands `compute-image-tag.mjs`. It is therefore the ONLY source of the vendored
|
|
2902
|
+
* commit that can agree with the tag CI pushes.
|
|
2903
|
+
*/
|
|
2904
|
+
export const CROSS_REPO_BUILD_INPUTS_PATH =
|
|
2905
|
+
'tooling/release/cross-repo-build-inputs.json';
|
|
2906
|
+
|
|
2907
|
+
/** Identity of one vendored input — the tuple that survives a pin bump. */
|
|
2908
|
+
export function crossRepoInputKey(input) {
|
|
2909
|
+
return [input.sourceRepo, input.sourcePath, input.destinationPath].join('\u0000');
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* Resolve the vendored inputs of ONE consumer repository at ONE commit.
|
|
2914
|
+
*
|
|
2915
|
+
* WHY THE COMMIT COMES FROM THE CONSUMER, NOT FROM THE INDEX
|
|
2916
|
+
*
|
|
2917
|
+
* The index's `crossRepoBuildInputs` carry no `sourceCommit`; they name only
|
|
2918
|
+
* WHICH content is vendored. Both this tooling and the parity gate used to
|
|
2919
|
+
* supply the missing commit from `runtimeSourceIndex.repositoryPins`, and CI
|
|
2920
|
+
* supplies it from the file above — so the two disagreed the moment a consumer
|
|
2921
|
+
* repository's mirror lagged the index by one pin, which is its NORMAL resting
|
|
2922
|
+
* state: the mirror is generated FROM the index and committed separately, so it
|
|
2923
|
+
* is stale by construction between the index moving and the consumer
|
|
2924
|
+
* regenerating.
|
|
2925
|
+
*
|
|
2926
|
+
* That is not a cosmetic disagreement. `crossRepo.sourceCommit` is framed into
|
|
2927
|
+
* the component KEY of the image-input hash, so a pin that differs by one
|
|
2928
|
+
* commit yields a different `contentHash` FOR BYTE-IDENTICAL VENDORED CONTENT —
|
|
2929
|
+
* a coordinate no CI run has ever pushed or ever will. Measured 2026-09-07 on
|
|
2930
|
+
* `origin/main`: five of the sixty-eight coordinates in the eight editions
|
|
2931
|
+
* (`backlog-api`, `channel-gateway-api`, `control-plane-api`,
|
|
2932
|
+
* `document-render-api` from repos/xema-community, and `biome-host-api` from
|
|
2933
|
+
* repos/xema-base) 404'd in the registry for exactly this reason, while the tags
|
|
2934
|
+
* their producing CI DID push were present and served on both platforms.
|
|
2935
|
+
*
|
|
2936
|
+
* Reading the index pin also imposed a requirement that cannot be satisfied.
|
|
2937
|
+
* `repos/xema-base` vendors from `repos/xema-community` and vice versa, so
|
|
2938
|
+
* "every mirror agrees with the index" is a mutual fixed point over a 2-cycle,
|
|
2939
|
+
* and bumping either side invalidates the other. No such pair exists: scanning
|
|
2940
|
+
* eighty commits of each on `origin/main` found ZERO mutually coherent pairs.
|
|
2941
|
+
* Reading the consumer's own record removes the fixed point entirely — an
|
|
2942
|
+
* image's identity depends only on the repository that builds it.
|
|
2943
|
+
*
|
|
2944
|
+
* It stays edition-INDEPENDENT, which is the property the previous design was
|
|
2945
|
+
* protecting: the record is a property of the consumer's COMMIT, not of any
|
|
2946
|
+
* edition's resolved commit map, so every edition shipping a subject still
|
|
2947
|
+
* derives the same `contentHash` for it.
|
|
2948
|
+
*/
|
|
2949
|
+
export function resolveCrossRepoVendoredInputs({
|
|
2950
|
+
indexInputs,
|
|
2951
|
+
consumerRepo,
|
|
2952
|
+
consumerRepoDir,
|
|
2953
|
+
consumerCommit,
|
|
2954
|
+
}) {
|
|
2955
|
+
const declared = indexInputs.filter(
|
|
2956
|
+
(input) => input.consumerRepo === consumerRepo,
|
|
2957
|
+
);
|
|
2958
|
+
if (declared.length === 0) {return [];}
|
|
2959
|
+
if (!consumerCommit) {
|
|
2960
|
+
throw new Error(
|
|
2961
|
+
`cross-repo build inputs are declared for "${consumerRepo}", but this ` +
|
|
2962
|
+
`distribution resolves no commit for it — the vendored content cannot ` +
|
|
2963
|
+
`be pinned, so the image cannot be addressed`,
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
let record;
|
|
2967
|
+
try {
|
|
2968
|
+
record = readGitJsonAtCommit(
|
|
2969
|
+
consumerRepoDir,
|
|
2970
|
+
CROSS_REPO_BUILD_INPUTS_PATH,
|
|
2971
|
+
consumerCommit,
|
|
2972
|
+
);
|
|
2973
|
+
} catch (cause) {
|
|
2974
|
+
throw new Error(
|
|
2975
|
+
`"${consumerRepo}" declares cross-repo build inputs but ` +
|
|
2976
|
+
`${CROSS_REPO_BUILD_INPUTS_PATH} is unreadable at ${consumerCommit}. ` +
|
|
2977
|
+
`That file is what its CI feeds to compute-image-tag.mjs; without it ` +
|
|
2978
|
+
`no coordinate this tooling derives can match the tag CI pushes.`,
|
|
2979
|
+
{ cause },
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
const mirrored = new Map();
|
|
2983
|
+
for (const input of record?.inputs ?? []) {
|
|
2984
|
+
mirrored.set(crossRepoInputKey(input), input.sourceCommit);
|
|
2985
|
+
}
|
|
2986
|
+
// Set equality in BOTH directions. A commit that LAGS is normal and is now
|
|
2987
|
+
// honoured; a differing input SET is a genuine declaration disagreement, and
|
|
2988
|
+
// it moves the hash just as surely — so it must fail rather than resolve to a
|
|
2989
|
+
// coordinate built from a different closure.
|
|
2990
|
+
const declaredKeys = new Set(declared.map((input) => crossRepoInputKey(input)));
|
|
2991
|
+
const resolved = declared.map((input) => {
|
|
2992
|
+
const sourceCommit = mirrored.get(crossRepoInputKey(input));
|
|
2993
|
+
if (!sourceCommit) {
|
|
2994
|
+
throw new Error(
|
|
2995
|
+
`the runtime image source index declares a cross-repo build input for ` +
|
|
2996
|
+
`"${consumerRepo}" vendoring "${input.sourceRepo}:${input.sourcePath}" ` +
|
|
2997
|
+
`to "${input.destinationPath}", but ${CROSS_REPO_BUILD_INPUTS_PATH} at ` +
|
|
2998
|
+
`${consumerCommit} declares no such input — its CI does not vendor it, ` +
|
|
2999
|
+
`so the coordinate this tooling would derive is one nothing builds`,
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
return {
|
|
3003
|
+
consumerService: input.consumerService ?? null,
|
|
3004
|
+
destinationPath: input.destinationPath,
|
|
3005
|
+
sourceRepoDir: sourcePath(input.sourceRepo),
|
|
3006
|
+
sourcePath: input.sourcePath,
|
|
3007
|
+
sourceCommit,
|
|
3008
|
+
};
|
|
3009
|
+
});
|
|
3010
|
+
for (const key of mirrored.keys()) {
|
|
3011
|
+
if (declaredKeys.has(key)) {continue;}
|
|
3012
|
+
const [sourceRepo, sourcePath, destinationPath] = key.split('\0');
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
`${CROSS_REPO_BUILD_INPUTS_PATH} at ${consumerCommit} vendors ` +
|
|
3015
|
+
`"${sourceRepo}:${sourcePath}" to "${destinationPath}" for ` +
|
|
3016
|
+
`"${consumerRepo}", which the runtime image source index does not ` +
|
|
3017
|
+
`declare — its CI bakes content this tooling does not hash`,
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
return resolved;
|
|
3021
|
+
}
|