@xemahq/xema 0.29.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +60 -389
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/edition.js +3 -3
  6. package/dist/commands/edition.js.map +1 -1
  7. package/dist/dev/infra-preflight.js +1 -1
  8. package/dist/dev/infra-preflight.js.map +1 -1
  9. package/dist/edition/verify-image-evidence.bundle.mjs +10030 -1491
  10. package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
  11. package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
  12. package/dist/runtime/runner/helm-cli.js.map +1 -1
  13. package/dist/runtime/store-submit.d.ts.map +1 -1
  14. package/dist/runtime/store-submit.js.map +1 -1
  15. package/edition/README.md +50 -271
  16. package/edition/adapter-bundled-versions.mjs +107 -0
  17. package/edition/adapter-host-versions.mjs +41 -0
  18. package/edition/assemble-producer-input.mjs +1 -1
  19. package/edition/authorization-base.mjs +18 -9
  20. package/edition/biome-content.mjs +6 -0
  21. package/edition/build-platform-service-index.mjs +2 -0
  22. package/edition/build-runtime-image-source-index.mjs +97 -9
  23. package/edition/catalogue.mjs +20 -13
  24. package/edition/checkout-indexed-source-repositories.mjs +41 -6
  25. package/edition/customer-definition.mjs +3 -20
  26. package/edition/develop-customer-edition.mjs +2 -2
  27. package/edition/develop-edition-resolve.mjs +10 -2
  28. package/edition/develop-edition.mjs +56 -62
  29. package/edition/develop-freeze.mjs +14 -3
  30. package/edition/dispatched-runtime-components.mjs +82 -0
  31. package/edition/dist-check.mjs +1 -0
  32. package/edition/distribution-evidence-set.mjs +32 -5
  33. package/edition/edition-channel.mjs +15 -1
  34. package/edition/edition-locks.mjs +16 -6
  35. package/edition/edition-protocol.mjs +30 -8
  36. package/edition/edition-workspace.mjs +8 -0
  37. package/edition/external-source-walk.mjs +1 -1
  38. package/edition/image-evidence.mjs +134 -26
  39. package/edition/kernel-capability-domain.mjs +30 -0
  40. package/edition/kernel-package.mjs +193 -0
  41. package/edition/lib.mjs +192 -4
  42. package/edition/oci-evidence-verbs.mjs +2 -2
  43. package/edition/operation-grant-admission.mjs +70 -0
  44. package/edition/operation-requirements.mjs +128 -0
  45. package/edition/plan-image-evidence-refresh.mjs +2 -2
  46. package/edition/platform-lock-readability.mjs +2 -2
  47. package/edition/platform-service-class.mjs +42 -0
  48. package/edition/platform-service-ownership.mjs +78 -0
  49. package/edition/produce-distribution-completeness-evidence.mjs +5 -0
  50. package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
  51. package/edition/release-catalogue.mjs +10 -3
  52. package/edition/remedy-command.mjs +2 -2
  53. package/edition/resolve-chart-set.mjs +1 -1
  54. package/edition/resolve-customer-edition.mjs +33 -34
  55. package/edition/resolve-distribution.mjs +70 -16
  56. package/edition/resolve-edition.mjs +3 -2
  57. package/edition/resolve-images.mjs +65 -4
  58. package/edition/seal-image-digests.mjs +17 -5
  59. package/edition/trust-policy-arming-verb.mjs +2 -10
  60. package/edition/verify-image-evidence.mjs +1 -0
  61. package/examples/README.md +3 -2
  62. package/package.json +4 -4
@@ -36,6 +36,7 @@ import { execFileSync } from 'node:child_process';
36
36
  import { writeFileSync } from 'node:fs';
37
37
  import { join, relative, sep } from 'node:path';
38
38
 
39
+ import { resolveDispatchedRuntimeComponents } from './dispatched-runtime-components.mjs';
39
40
  import {
40
41
  assertCanonicalDistributionId,
41
42
  distributionOutputPath,
@@ -281,7 +282,7 @@ export function resolveDistributionRequiredPlatforms(schemas, id) {
281
282
  * the fully-qualified `imageRef`. It cannot travel in the lock to reach the
282
283
  * resolver — `DistributionLockSchema` is `.strict()` and learned that key only
283
284
  * in `@xemahq/contracts` 0.10.0, so a pinned image resolving `^0.9.x` refuses
284
- * the whole lock at boot. Persisting it took presenca-dev's three lock
285
+ * the whole lock at boot. Persisting it took a customer environment's three lock
285
286
  * consumers into CrashLoopBackOff on 2026-09-17.
286
287
  *
287
288
  * So the producer reads its OWN declaration, and `requiredPlatforms` one
@@ -396,8 +397,11 @@ function missingArtifactDependencyMessage(lock, serviceName, binding, biomeIndex
396
397
  * refuse. What DOES refuse is a SELECTED service whose declared artifact
397
398
  * dependency is absent from the closure (see `missingArtifactDependencyMessage`).
398
399
  *
399
- * `options.biomeIndex` is optional and used ONLY to name, in a refusal, which
400
- * biome declares the missing artifact. It never changes a resolved roster.
400
+ * `options.biomeIndex` is the first-party biome index. It names, in a refusal,
401
+ * which biome declares a missing artifact, and it carries each rostered biome's
402
+ * `dispatchesRuntimeComponents`: the runtime components that biome launches,
403
+ * whose images enter this roster as runtime-component rows although their
404
+ * biomes are not rostered (see `dispatched-runtime-components.mjs`).
401
405
  */
402
406
  export function resolveImageStructure(
403
407
  lock,
@@ -414,6 +418,12 @@ export function resolveImageStructure(
414
418
  `and there is no default; a silent default is the bug this replaced`,
415
419
  );
416
420
  }
421
+ if (!Array.isArray(biomeIndex)) {
422
+ throw new Error(
423
+ `distribution "${lock.distributionId}" resolved its images without the biome index — the index carries ` +
424
+ 'the runtime components a rostered biome dispatches, so a roster without it can omit images',
425
+ );
426
+ }
417
427
  const subjects = new Map();
418
428
  const webBiomes = [];
419
429
  const platformServiceBindings = [];
@@ -505,7 +515,7 @@ export function resolveImageStructure(
505
515
  // is `.strict()` and learned the key only in 0.10.0.
506
516
  //
507
517
  // Removing it from the lock WITHOUT moving this read is what composed
508
- // `<customer-registry>/pai-core-api` instead of `<customer-registry>/presenca-pai-core-api`:
518
+ // `<customer-registry>/pai-core-api` instead of `<customer-registry>/<customer>-pai-core-api`:
509
519
  // the `?? ''` swallowed the absence, so an unqualified name in a shared
510
520
  // organisation namespace resolved silently. That is worse than a 404 —
511
521
  // the first collision overwrites another product's tags.
@@ -604,6 +614,57 @@ export function resolveImageStructure(
604
614
  }
605
615
  }
606
616
 
617
+ // Runtime components a rostered biome DISPATCHES enter the roster as
618
+ // runtime-component rows built from the TARGET biome's index entry, at its
619
+ // repository pin, exactly like a rostered biome's own runtime component. The
620
+ // owner is the rostered biome that dispatches it: the selection that brings
621
+ // the image into this edition. The target biome stays unrostered.
622
+ //
623
+ // A rostered biome's edges are read from ITS index entry, which must describe
624
+ // the very biome the lock pins; one from a declared source is refused at
625
+ // distribution resolve if it dispatches, and the first-party index carries
626
+ // none, so it is not looked up here.
627
+ const dispatchers = lock.biomes
628
+ .filter((biome) => biome.source === undefined && biome.target === 'server')
629
+ .map((biome) => {
630
+ const entries = biomeIndex.filter(
631
+ (entry) =>
632
+ entry.id === biome.id &&
633
+ entry.target === biome.target &&
634
+ entry.repo === biome.repo &&
635
+ entry.source === undefined,
636
+ );
637
+ if (entries.length !== 1 || entries[0].sourceManifestHash !== biome.sourceManifestHash) {
638
+ throw new Error(
639
+ `distribution "${lock.distributionId}" biome "${biome.id}" (${biome.repo}, ${biome.sourceManifestHash}) ` +
640
+ `has ${entries.length === 1 ? 'a stale' : `${entries.length}`} biome index ${entries.length === 1 ? 'entry' : 'entries'}, ` +
641
+ 'so the runtime components it dispatches cannot be read — re-resolve the edition from the current index',
642
+ );
643
+ }
644
+ return entries[0];
645
+ });
646
+ for (const dispatched of resolveDispatchedRuntimeComponents(
647
+ `distribution "${lock.distributionId}"`,
648
+ dispatchers,
649
+ biomeIndex,
650
+ )) {
651
+ const { target } = dispatched;
652
+ addSubject(subjects, {
653
+ kind: enums.RuntimeImageSubjectKind.RuntimeComponent,
654
+ id: dispatched.image,
655
+ imageRef: imageRefForSubject(dispatched.image, firstPartyImageRegistry(), ''),
656
+ repo: target.repo,
657
+ path: target.path,
658
+ sourceCommit: requireRepositoryPin(target.repo).sourceCommit,
659
+ sourceManifestHash: target.sourceManifestHash,
660
+ requiredPlatforms,
661
+ owners: dispatched.consumers.map((biomeId) => ({
662
+ kind: enums.RuntimeImageOwnerKind.Biome,
663
+ biomeId,
664
+ })),
665
+ });
666
+ }
667
+
607
668
  const runtimeBindings = new Map(
608
669
  runtimeSourceIndex.platformServiceBindings.map((binding) => [
609
670
  binding.platformService,
@@ -112,18 +112,20 @@
112
112
  import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
113
113
  import { join } from 'node:path';
114
114
 
115
+ import { adapterHostRow, assertAdapterBundlesMatchHost, fetchRegistryManifest, lockMountsAdapters } from './adapter-bundled-versions.mjs';
115
116
  import { sealLock, verifyBiomeContentHashes } from './biome-content.mjs';
116
117
  import { fetchCatalogue } from './catalogue-fetch.mjs';
117
118
  import { readCataloguePin } from './catalogue-pin.mjs';
118
119
  import { CATALOGUE_TOKEN_ENV_FLAG, CATALOGUE_USERNAME_FLAG, DEFINITION_FLAG } from './customer-flags.mjs';
119
120
  import { EditionChannel } from './edition-constants.mjs';
120
- import { WorkspaceRoot, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
121
+ import { WorkspaceRoot, biomeIndexPath, configureEditionWorkspaceFromProcess, editionsRoot, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
121
122
  import {
122
123
  assertCoordinatesFinal,
123
124
  collectFleetPlatformRequirements,
124
125
  isNotFinalYet,
125
126
  readFleetImageLocks,
126
127
  } from './fleet-coordinate-finality.mjs';
128
+ import { adoptCatalogueKernel } from './kernel-package.mjs';
127
129
  import { loadKernelDistributionSchemas, serializeJson } from './lib.mjs';
128
130
  import { assertPlatformCanParseLock } from './platform-lock-readability.mjs';
129
131
  import { assertReleaseEditionTree } from './release-channel-guard.mjs';
@@ -138,7 +140,8 @@ import {
138
140
  if (isEntryModule(import.meta.url)) {
139
141
  // The customer form (it names --definition) reads exactly these roots - the data root (the vendor's
140
142
  // runtime-image-source-index.json), the editions root, the sources root (the readability check of the
141
- // vendor repositories), the kernel and the cache (external source trees) - and refuses any other.
143
+ // vendor repositories) and the cache (external source trees, and the catalogue's kernel from npm) - and
144
+ // refuses any other, the kernel root included. A vendor seal takes the kernel root too.
142
145
  let namesDefinition;
143
146
  try {
144
147
  namesDefinition = parseSealArgs(process.argv.slice(2)).catalogueFlags.has(DEFINITION_FLAG);
@@ -147,7 +150,9 @@ if (isEntryModule(import.meta.url)) {
147
150
  process.exit(1);
148
151
  }
149
152
  configureEditionWorkspaceFromProcess(
150
- [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache],
153
+ namesDefinition
154
+ ? [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Cache]
155
+ : [WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache],
151
156
  { refuseOthers: namesDefinition },
152
157
  );
153
158
  }
@@ -479,7 +484,7 @@ function replaceLocksTogether(documents) {
479
484
  * can leave (images sealed, distribution unsealed) is refused by every reader of
480
485
  * a sealed edition.
481
486
  */
482
- export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest }) {
487
+ export async function sealEditionLocks({ distributionId, root, digests = new Map(), check = false, label = 'dist:seal-digests', vendoredCommit, catalogue, catalogueDigest, fetchManifest = fetchRegistryManifest }) {
483
488
  const schemas = await loadKernelDistributionSchemas();
484
489
  const lockPath = join(root, distributionId, 'distribution.lock.json');
485
490
  const imagesPath = join(root, distributionId, 'images.lock.json');
@@ -505,6 +510,11 @@ export async function sealEditionLocks({ distributionId, root, digests = new Map
505
510
  const imageLock = schemas.parseImageLock(rawImages);
506
511
  const pinned = vendoredCommit ?? ((repo) => createPinnedSourceAuthority({ pins: committedRepositoryPins() }).vendoredSourceCommitForRepository(repo));
507
512
  const lockDocument = raw !== null && typeof raw === 'object' && 'unsealedSchema' in raw ? schemas.parseUnsealedDistributionLock(raw) : schemas.parseDistributionLock(raw);
513
+ if (lockMountsAdapters(lockDocument)) {
514
+ // The adapter host's row: the verified catalogue's for a customer, the data root's biome index for the vendor.
515
+ const rows = catalogue === undefined ? JSON.parse(readFileSync(biomeIndexPath(), 'utf8')) : catalogue.biomes;
516
+ await assertAdapterBundlesMatchHost({ lock: lockDocument, hostRow: adapterHostRow(rows), fetchManifest });
517
+ }
508
518
  const result = await sealOrVerifyLock({
509
519
  schemas,
510
520
  lock: lockDocument,
@@ -657,6 +667,8 @@ async function main() {
657
667
  : undefined;
658
668
  const catalogue = fetched?.catalogue;
659
669
  const catalogueDigest = fetched?.digest;
670
+ // A customer seal reads the kernel of its catalogue (from npm), proved its contract authority, before any kernel schema is loaded.
671
+ if (catalogue !== undefined) {await adoptCatalogueKernel(catalogue);}
660
672
  const imageLock = await loadImageLockForVerification(distributionId, root);
661
673
 
662
674
  // Throws on ANY unresolvable subject or missing required platform, before a
@@ -691,7 +703,7 @@ async function main() {
691
703
  // `DistributionLockSchema` is `.strict()` — so a key the plane knows and a
692
704
  // pinned repository does not makes the whole artifact unreadable AT BOOT.
693
705
  // That is not hypothetical: on 2026-09-17 `imageNamePrefix` did exactly this
694
- // to three presenca-dev services at once, and the edition had sealed,
706
+ // to three a customer environment services at once, and the edition had sealed,
695
707
  // published and installed perfectly first.
696
708
  //
697
709
  // It is asked of every pinned repository that SHIPS AN IMAGE INTO THIS
@@ -9,7 +9,7 @@
9
9
  // usage error. A data root that declares no policy is `unarmed`.
10
10
  // It reads the policy the way `xema edition check` does and never writes.
11
11
 
12
- import { EditionChannel } from './edition-channel.mjs';
12
+ import { EditionChannel, parseChannelArgs } from './edition-channel.mjs';
13
13
  import {
14
14
  WorkspaceRoot,
15
15
  configureEditionWorkspaceFromArgv,
@@ -23,14 +23,6 @@ export const ARMING_USAGE = `usage: xema edition arming --channel <${Object.valu
23
23
  /** The process exit codes of the verb. */
24
24
  export const ArmingExit = Object.freeze({ Armed: 0, NotArmed: 1, Usage: 2 });
25
25
 
26
- /** The channel of `--channel <channel>` among `rest`; anything else is a usage error. */
27
- export function parseArmingArgs(rest) {
28
- if (rest.length !== 2 || rest[0] !== '--channel' || !Object.values(EditionChannel).includes(rest[1])) {
29
- throw new Error(`--channel <${Object.values(EditionChannel).join('|')}> is required and nothing else is accepted (got ${rest.length === 0 ? 'nothing' : JSON.stringify(rest.join(' '))})`);
30
- }
31
- return { channel: rest[1] };
32
- }
33
-
34
26
  /**
35
27
  * Judge the configured data root's policy on `channel`, writing the one JSON
36
28
  * line to `stdout` and each unmet entry to `stderr`. Returns the exit code.
@@ -54,7 +46,7 @@ async function main() {
54
46
  let channel;
55
47
  try {
56
48
  const rest = configureEditionWorkspaceFromArgv(process.argv.slice(2), [WorkspaceRoot.Data, WorkspaceRoot.Kernel]);
57
- ({ channel } = parseArmingArgs(rest));
49
+ ({ channel } = parseChannelArgs(rest));
58
50
  } catch (error) {
59
51
  process.stderr.write(`arming: ${error.message}\n${ARMING_USAGE}\n`);
60
52
  process.exitCode = ArmingExit.Usage;
@@ -397,6 +397,7 @@ export async function verifyEvidence({ argv, env, bundlePath }) {
397
397
  completenessStatementDigest: result.completenessStatementDigest,
398
398
  customerId: options.customerId,
399
399
  distributionId: options.distributionId,
400
+ distributionLock: result.distributionLock,
400
401
  channel,
401
402
  mode: options.mode,
402
403
  targetRegistry: options.targetRegistry,
@@ -1,5 +1,6 @@
1
1
  # `xema.config.yaml` examples
2
2
 
3
+
3
4
  Copy one of these to `xema.config.yaml` (or `~/.xema/config.yaml`) and edit:
4
5
 
5
6
  ```bash
@@ -14,6 +15,6 @@ xema up
14
15
  | [`03-byo-infra.yaml`](./03-byo-infra.yaml) | Bring-your-own Postgres / Keycloak / object store; launcher runs only the rest. |
15
16
  | [`04-subset-knowledge.yaml`](./04-subset-knowledge.yaml) | Run only a slice (tiers / services) to save resources. |
16
17
  | [`05-appliance-helm.yaml`](./05-appliance-helm.yaml) | Deploy to Kubernetes / an appliance via Helm. |
18
+ | [`06-biome-sources.yaml`](./06-biome-sources.yaml) | Pull biomes from workspace, distribution and git sources. |
17
19
 
18
- Precedence: `--config <path>` → `./xema.config.yaml` → `~/.xema/config.yaml` →
19
- built-in defaults. Every value shown is the default unless noted.
20
+ Config precedence: `--config <path>`, then `./xema.config.yaml`, then `~/.xema/config.yaml`, then built-in defaults.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.29.1",
3
+ "version": "0.31.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
7
7
  "directory": "apps/xema-cli"
8
8
  },
9
9
  "license": "Apache-2.0",
10
- "description": "Xema OS global CLI (plan v4.3 §6 Phase J). Single binary day-0 onboarding: `npm i -g @xemahq/xema && xema dev` boots the whole stack; `xema biome scaffold` / `install` / `publish` / `lint` author and ship biomes; `xema run <capability>` invokes through the local capability-router. Multi-platform: works without pnpm or any monorepo clone.",
10
+ "description": "Xema global CLI. Single binary day-0 onboarding: `npm i -g @xemahq/xema && xema dev` boots the whole stack; `xema biome scaffold` / `install` / `publish` / `lint` author and ship biomes; `xema run <capability>` invokes through the local capability-router. Multi-platform: works without pnpm or any monorepo clone.",
11
11
  "type": "commonjs",
12
12
  "main": "dist/cli.js",
13
13
  "exports": {
@@ -54,7 +54,7 @@
54
54
  "@xemahq/distribution-source-hash": "^0.3.0",
55
55
  "@xemahq/dsl": "^0.23.0",
56
56
  "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.76.0",
57
+ "@xemahq/contracts": "^0.80.0",
58
58
  "@xemahq/kernel-state-runtime": "^0.8.0",
59
59
  "@xemahq/license-verifier": "^0.2.0",
60
60
  "@xemahq/service-registry-nest": "^0.12.2",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:82760f14feea4fe0626220e0f71344375964f15d36201d1e7000ebfb6b2e4ecd"
95
+ "sourceDigest": "sha256:2fc7a76e728752cf14eeb410921fb5c89cbffc9c3c398116a3c37dcd740587ea"
96
96
  }
97
97
  }