@xemahq/xema 0.59.1 → 0.60.1
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 +1 -2
- package/dist/appliance-owner.d.ts.map +1 -1
- package/dist/appliance-owner.js +6 -6
- package/dist/appliance-owner.js.map +1 -1
- package/dist/assets/appliance-base/authorization.lock.json +5186 -0
- package/dist/assets/appliance-base/distribution.lock.json +760 -279
- package/dist/assets/appliance-base/images.lock.json +576 -513
- package/dist/assets/appliance-lean/authorization.lock.json +4648 -0
- package/dist/assets/appliance-lean/distribution.lock.json +641 -186
- package/dist/assets/appliance-lean/images.lock.json +574 -512
- package/dist/assets/boot-graph.json +33 -14
- package/dist/assets/distribution-assets.manifest.json +23 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -9
- package/dist/cli.js.map +1 -1
- package/dist/commands/biome-dev.d.ts.map +1 -1
- package/dist/commands/biome-dev.js +41 -137
- package/dist/commands/biome-dev.js.map +1 -1
- package/dist/commands/biome-lifecycle.d.ts +24 -0
- package/dist/commands/biome-lifecycle.d.ts.map +1 -0
- package/dist/commands/biome-lifecycle.js +253 -0
- package/dist/commands/biome-lifecycle.js.map +1 -0
- package/dist/commands/biome.d.ts +2 -4
- package/dist/commands/biome.d.ts.map +1 -1
- package/dist/commands/biome.js +61 -46
- package/dist/commands/biome.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +96 -8
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +13 -20
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +14 -21
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +6 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/installation-tenancy.d.ts +11 -0
- package/dist/commands/installation-tenancy.d.ts.map +1 -0
- package/dist/commands/installation-tenancy.js +308 -0
- package/dist/commands/installation-tenancy.js.map +1 -0
- package/dist/commands/installation.d.ts.map +1 -1
- package/dist/commands/installation.js +19 -5
- package/dist/commands/installation.js.map +1 -1
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +41 -8
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/{dev.d.ts → mirror.d.ts} +1 -1
- package/dist/commands/mirror.d.ts.map +1 -0
- package/dist/commands/mirror.js +132 -0
- package/dist/commands/mirror.js.map +1 -0
- package/dist/commands/source.d.ts.map +1 -1
- package/dist/commands/source.js +3 -2
- package/dist/commands/source.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +40 -21
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +36 -4
- package/dist/commands/up.js.map +1 -1
- package/dist/dev/infra-preflight.d.ts.map +1 -1
- package/dist/dev/infra-preflight.js +0 -4
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/lib/confirm.d.ts +3 -0
- package/dist/lib/confirm.d.ts.map +1 -0
- package/dist/lib/confirm.js +19 -0
- package/dist/lib/confirm.js.map +1 -0
- package/dist/lib/output-format.d.ts +6 -0
- package/dist/lib/output-format.d.ts.map +1 -0
- package/dist/lib/output-format.js +20 -0
- package/dist/lib/output-format.js.map +1 -0
- package/dist/mirror/config/mirror-config-document.d.ts +18 -0
- package/dist/mirror/config/mirror-config-document.d.ts.map +1 -0
- package/dist/mirror/config/mirror-config-document.js +57 -0
- package/dist/mirror/config/mirror-config-document.js.map +1 -0
- package/dist/mirror/config/mirror-config-schema.d.ts +54 -0
- package/dist/mirror/config/mirror-config-schema.d.ts.map +1 -0
- package/dist/mirror/config/mirror-config-schema.js +80 -0
- package/dist/mirror/config/mirror-config-schema.js.map +1 -0
- package/dist/mirror/kit/delta.d.ts +4 -0
- package/dist/mirror/kit/delta.d.ts.map +1 -0
- package/dist/mirror/kit/delta.js +41 -0
- package/dist/mirror/kit/delta.js.map +1 -0
- package/dist/mirror/kit/export-kit.d.ts +16 -0
- package/dist/mirror/kit/export-kit.d.ts.map +1 -0
- package/dist/mirror/kit/export-kit.js +112 -0
- package/dist/mirror/kit/export-kit.js.map +1 -0
- package/dist/mirror/kit/import-kit.d.ts +9 -0
- package/dist/mirror/kit/import-kit.d.ts.map +1 -0
- package/dist/mirror/kit/import-kit.js +166 -0
- package/dist/mirror/kit/import-kit.js.map +1 -0
- package/dist/mirror/kit/kit-archive.d.ts +4 -0
- package/dist/mirror/kit/kit-archive.d.ts.map +1 -0
- package/dist/mirror/kit/kit-archive.js +38 -0
- package/dist/mirror/kit/kit-archive.js.map +1 -0
- package/dist/mirror/kit/kit-layout.d.ts +10 -0
- package/dist/mirror/kit/kit-layout.d.ts.map +1 -0
- package/dist/mirror/kit/kit-layout.js +19 -0
- package/dist/mirror/kit/kit-layout.js.map +1 -0
- package/dist/mirror/kit/kit-sums.d.ts +5 -0
- package/dist/mirror/kit/kit-sums.d.ts.map +1 -0
- package/dist/mirror/kit/kit-sums.js +63 -0
- package/dist/mirror/kit/kit-sums.js.map +1 -0
- package/dist/mirror/kit/layout-sync.d.ts +8 -0
- package/dist/mirror/kit/layout-sync.d.ts.map +1 -0
- package/dist/mirror/kit/layout-sync.js +58 -0
- package/dist/mirror/kit/layout-sync.js.map +1 -0
- package/dist/mirror/plan/bundle-content.d.ts +10 -0
- package/dist/mirror/plan/bundle-content.d.ts.map +1 -0
- package/dist/mirror/plan/bundle-content.js +61 -0
- package/dist/mirror/plan/bundle-content.js.map +1 -0
- package/dist/mirror/plan/declaration-file.d.ts +9 -0
- package/dist/mirror/plan/declaration-file.d.ts.map +1 -0
- package/dist/mirror/plan/declaration-file.js +27 -0
- package/dist/mirror/plan/declaration-file.js.map +1 -0
- package/dist/mirror/plan/edition-inputs.d.ts +57 -0
- package/dist/mirror/plan/edition-inputs.d.ts.map +1 -0
- package/dist/mirror/plan/edition-inputs.js +44 -0
- package/dist/mirror/plan/edition-inputs.js.map +1 -0
- package/dist/mirror/plan/edition-reader.d.ts +9 -0
- package/dist/mirror/plan/edition-reader.d.ts.map +1 -0
- package/dist/mirror/plan/edition-reader.js +47 -0
- package/dist/mirror/plan/edition-reader.js.map +1 -0
- package/dist/mirror/plan/image-content.d.ts +30 -0
- package/dist/mirror/plan/image-content.d.ts.map +1 -0
- package/dist/mirror/plan/image-content.js +120 -0
- package/dist/mirror/plan/image-content.js.map +1 -0
- package/dist/mirror/plan/npm-lockfile.d.ts +12 -0
- package/dist/mirror/plan/npm-lockfile.d.ts.map +1 -0
- package/dist/mirror/plan/npm-lockfile.js +106 -0
- package/dist/mirror/plan/npm-lockfile.js.map +1 -0
- package/dist/mirror/plan/oci-reads.d.ts +22 -0
- package/dist/mirror/plan/oci-reads.d.ts.map +1 -0
- package/dist/mirror/plan/oci-reads.js +43 -0
- package/dist/mirror/plan/oci-reads.js.map +1 -0
- package/dist/mirror/plan/plan-command.d.ts +22 -0
- package/dist/mirror/plan/plan-command.d.ts.map +1 -0
- package/dist/mirror/plan/plan-command.js +81 -0
- package/dist/mirror/plan/plan-command.js.map +1 -0
- package/dist/mirror/plan/plan-file.d.ts +13 -0
- package/dist/mirror/plan/plan-file.d.ts.map +1 -0
- package/dist/mirror/plan/plan-file.js +63 -0
- package/dist/mirror/plan/plan-file.js.map +1 -0
- package/dist/mirror/plan/resolve-content-set.d.ts +16 -0
- package/dist/mirror/plan/resolve-content-set.d.ts.map +1 -0
- package/dist/mirror/plan/resolve-content-set.js +90 -0
- package/dist/mirror/plan/resolve-content-set.js.map +1 -0
- package/dist/mirror/preflight/glob.d.ts +2 -0
- package/dist/mirror/preflight/glob.d.ts.map +1 -0
- package/dist/mirror/preflight/glob.js +59 -0
- package/dist/mirror/preflight/glob.js.map +1 -0
- package/dist/mirror/preflight/harbor-api.d.ts +27 -0
- package/dist/mirror/preflight/harbor-api.d.ts.map +1 -0
- package/dist/mirror/preflight/harbor-api.js +39 -0
- package/dist/mirror/preflight/harbor-api.js.map +1 -0
- package/dist/mirror/preflight/preflight.d.ts +18 -0
- package/dist/mirror/preflight/preflight.d.ts.map +1 -0
- package/dist/mirror/preflight/preflight.js +41 -0
- package/dist/mirror/preflight/preflight.js.map +1 -0
- package/dist/mirror/preflight/tag-moves.d.ts +16 -0
- package/dist/mirror/preflight/tag-moves.d.ts.map +1 -0
- package/dist/mirror/preflight/tag-moves.js +28 -0
- package/dist/mirror/preflight/tag-moves.js.map +1 -0
- package/dist/mirror/preflight/write-probe.d.ts +10 -0
- package/dist/mirror/preflight/write-probe.d.ts.map +1 -0
- package/dist/mirror/preflight/write-probe.js +36 -0
- package/dist/mirror/preflight/write-probe.js.map +1 -0
- package/dist/mirror/target/extras.d.ts +16 -0
- package/dist/mirror/target/extras.d.ts.map +1 -0
- package/dist/mirror/target/extras.js +47 -0
- package/dist/mirror/target/extras.js.map +1 -0
- package/dist/mirror/target/npm-registry.d.ts +20 -0
- package/dist/mirror/target/npm-registry.d.ts.map +1 -0
- package/dist/mirror/target/npm-registry.js +67 -0
- package/dist/mirror/target/npm-registry.js.map +1 -0
- package/dist/mirror/target/npm-sync.d.ts +15 -0
- package/dist/mirror/target/npm-sync.d.ts.map +1 -0
- package/dist/mirror/target/npm-sync.js +44 -0
- package/dist/mirror/target/npm-sync.js.map +1 -0
- package/dist/mirror/target/oci-items.d.ts +27 -0
- package/dist/mirror/target/oci-items.d.ts.map +1 -0
- package/dist/mirror/target/oci-items.js +79 -0
- package/dist/mirror/target/oci-items.js.map +1 -0
- package/dist/mirror/target/oci-sync.d.ts +20 -0
- package/dist/mirror/target/oci-sync.d.ts.map +1 -0
- package/dist/mirror/target/oci-sync.js +93 -0
- package/dist/mirror/target/oci-sync.js.map +1 -0
- package/dist/mirror/target/receipt-store.d.ts +13 -0
- package/dist/mirror/target/receipt-store.d.ts.map +1 -0
- package/dist/mirror/target/receipt-store.js +61 -0
- package/dist/mirror/target/receipt-store.js.map +1 -0
- package/dist/mirror/target/receipt.d.ts +13 -0
- package/dist/mirror/target/receipt.d.ts.map +1 -0
- package/dist/mirror/target/receipt.js +25 -0
- package/dist/mirror/target/receipt.js.map +1 -0
- package/dist/mirror/target/seed-push.d.ts +15 -0
- package/dist/mirror/target/seed-push.d.ts.map +1 -0
- package/dist/mirror/target/seed-push.js +20 -0
- package/dist/mirror/target/seed-push.js.map +1 -0
- package/dist/mirror/target/target-access.d.ts +7 -0
- package/dist/mirror/target/target-access.d.ts.map +1 -0
- package/dist/mirror/target/target-access.js +17 -0
- package/dist/mirror/target/target-access.js.map +1 -0
- package/dist/mirror/target/target-commands.d.ts +21 -0
- package/dist/mirror/target/target-commands.d.ts.map +1 -0
- package/dist/mirror/target/target-commands.js +134 -0
- package/dist/mirror/target/target-commands.js.map +1 -0
- package/dist/mirror/target/target-context.d.ts +11 -0
- package/dist/mirror/target/target-context.d.ts.map +1 -0
- package/dist/mirror/target/target-context.js +31 -0
- package/dist/mirror/target/target-context.js.map +1 -0
- package/dist/mirror/target/target-ports.d.ts +18 -0
- package/dist/mirror/target/target-ports.d.ts.map +1 -0
- package/dist/mirror/target/target-ports.js +49 -0
- package/dist/mirror/target/target-ports.js.map +1 -0
- package/dist/mirror/target/target-state.d.ts +22 -0
- package/dist/mirror/target/target-state.d.ts.map +1 -0
- package/dist/mirror/target/target-state.js +41 -0
- package/dist/mirror/target/target-state.js.map +1 -0
- package/dist/runtime/biome-dev.d.ts +0 -16
- package/dist/runtime/biome-dev.d.ts.map +1 -1
- package/dist/runtime/biome-dev.js +0 -53
- package/dist/runtime/biome-dev.js.map +1 -1
- package/dist/runtime/biome-host/biome-directory-packer.d.ts +3 -0
- package/dist/runtime/biome-host/biome-directory-packer.d.ts.map +1 -0
- package/dist/runtime/biome-host/biome-directory-packer.js +12 -0
- package/dist/runtime/biome-host/biome-directory-packer.js.map +1 -0
- package/dist/runtime/biome-host/bundle-packer.d.ts +8 -0
- package/dist/runtime/biome-host/bundle-packer.d.ts.map +1 -0
- package/dist/runtime/biome-host/bundle-packer.js +3 -0
- package/dist/runtime/biome-host/bundle-packer.js.map +1 -0
- package/dist/runtime/biome-host/from-spec.d.ts +33 -0
- package/dist/runtime/biome-host/from-spec.d.ts.map +1 -0
- package/dist/runtime/biome-host/from-spec.js +70 -0
- package/dist/runtime/biome-host/from-spec.js.map +1 -0
- package/dist/runtime/biome-host/lifecycle-client.d.ts +20 -0
- package/dist/runtime/biome-host/lifecycle-client.d.ts.map +1 -0
- package/dist/runtime/biome-host/lifecycle-client.js +197 -0
- package/dist/runtime/biome-host/lifecycle-client.js.map +1 -0
- package/dist/runtime/biome-host/lifecycle-contract.d.ts +95 -0
- package/dist/runtime/biome-host/lifecycle-contract.d.ts.map +1 -0
- package/dist/runtime/biome-host/lifecycle-contract.js +61 -0
- package/dist/runtime/biome-host/lifecycle-contract.js.map +1 -0
- package/dist/runtime/biome-host/lifecycle-flow.d.ts +29 -0
- package/dist/runtime/biome-host/lifecycle-flow.d.ts.map +1 -0
- package/dist/runtime/biome-host/lifecycle-flow.js +106 -0
- package/dist/runtime/biome-host/lifecycle-flow.js.map +1 -0
- package/dist/runtime/biome-host/plan-sources.d.ts +4 -0
- package/dist/runtime/biome-host/plan-sources.d.ts.map +1 -0
- package/dist/runtime/biome-host/plan-sources.js +26 -0
- package/dist/runtime/biome-host/plan-sources.js.map +1 -0
- package/dist/runtime/biome-host/relay/bundle-fetch.d.ts +12 -0
- package/dist/runtime/biome-host/relay/bundle-fetch.d.ts.map +1 -0
- package/dist/runtime/biome-host/relay/bundle-fetch.js +76 -0
- package/dist/runtime/biome-host/relay/bundle-fetch.js.map +1 -0
- package/dist/runtime/biome-host/relay/declared-scheme.d.ts +3 -0
- package/dist/runtime/biome-host/relay/declared-scheme.d.ts.map +1 -0
- package/dist/runtime/biome-host/relay/declared-scheme.js +14 -0
- package/dist/runtime/biome-host/relay/declared-scheme.js.map +1 -0
- package/dist/runtime/biome-host/relay/npm-token.d.ts +4 -0
- package/dist/runtime/biome-host/relay/npm-token.d.ts.map +1 -0
- package/dist/runtime/biome-host/relay/npm-token.js +16 -0
- package/dist/runtime/biome-host/relay/npm-token.js.map +1 -0
- package/dist/runtime/biome-host/relay/relay-flow.d.ts +16 -0
- package/dist/runtime/biome-host/relay/relay-flow.d.ts.map +1 -0
- package/dist/runtime/biome-host/relay/relay-flow.js +48 -0
- package/dist/runtime/biome-host/relay/relay-flow.js.map +1 -0
- package/dist/runtime/biome-host/source-staging.d.ts +17 -0
- package/dist/runtime/biome-host/source-staging.d.ts.map +1 -0
- package/dist/runtime/biome-host/source-staging.js +80 -0
- package/dist/runtime/biome-host/source-staging.js.map +1 -0
- package/dist/runtime/biome-host/sse.d.ts +2 -0
- package/dist/runtime/biome-host/sse.d.ts.map +1 -0
- package/dist/runtime/biome-host/sse.js +33 -0
- package/dist/runtime/biome-host/sse.js.map +1 -0
- package/dist/runtime/biome-local-install.js +1 -1
- package/dist/runtime/biome-local-install.js.map +1 -1
- package/dist/runtime/browser-origins.d.ts +9 -0
- package/dist/runtime/browser-origins.d.ts.map +1 -1
- package/dist/runtime/browser-origins.js +10 -0
- package/dist/runtime/browser-origins.js.map +1 -1
- package/dist/runtime/config/config-document.d.ts +10 -0
- package/dist/runtime/config/config-document.d.ts.map +1 -0
- package/dist/runtime/config/config-document.js +56 -0
- package/dist/runtime/config/config-document.js.map +1 -0
- package/dist/runtime/config/config-migrations.d.ts +21 -0
- package/dist/runtime/config/config-migrations.d.ts.map +1 -0
- package/dist/runtime/config/config-migrations.js +102 -0
- package/dist/runtime/config/config-migrations.js.map +1 -0
- package/dist/runtime/config/config-paths.d.ts +3 -0
- package/dist/runtime/config/config-paths.d.ts.map +1 -0
- package/dist/runtime/config/config-paths.js +9 -0
- package/dist/runtime/config/config-paths.js.map +1 -0
- package/dist/runtime/config/dev-mode.d.ts +0 -1
- package/dist/runtime/config/dev-mode.d.ts.map +1 -1
- package/dist/runtime/config/dev-mode.js +11 -27
- package/dist/runtime/config/dev-mode.js.map +1 -1
- package/dist/runtime/config/init-presets.js +5 -9
- package/dist/runtime/config/init-presets.js.map +1 -1
- package/dist/runtime/config/launch-config-help.d.ts +4 -0
- package/dist/runtime/config/launch-config-help.d.ts.map +1 -0
- package/dist/runtime/config/launch-config-help.js +42 -0
- package/dist/runtime/config/launch-config-help.js.map +1 -0
- package/dist/runtime/config/launch-config.d.ts +60 -0
- package/dist/runtime/config/launch-config.d.ts.map +1 -0
- package/dist/runtime/config/launch-config.js +123 -0
- package/dist/runtime/config/launch-config.js.map +1 -0
- package/dist/runtime/config/launch-schema.d.ts +18 -0
- package/dist/runtime/config/launch-schema.d.ts.map +1 -0
- package/dist/runtime/config/launch-schema.js +31 -0
- package/dist/runtime/config/launch-schema.js.map +1 -0
- package/dist/runtime/config/loader.d.ts.map +1 -1
- package/dist/runtime/config/loader.js +4 -14
- package/dist/runtime/config/loader.js.map +1 -1
- package/dist/runtime/config/schema.d.ts +43 -38
- package/dist/runtime/config/schema.d.ts.map +1 -1
- package/dist/runtime/config/schema.js +17 -46
- package/dist/runtime/config/schema.js.map +1 -1
- package/dist/runtime/config/sources-config.d.ts +0 -2
- package/dist/runtime/config/sources-config.d.ts.map +1 -1
- package/dist/runtime/config/sources-config.js +10 -29
- package/dist/runtime/config/sources-config.js.map +1 -1
- package/dist/runtime/config/versioned-document.d.ts +26 -0
- package/dist/runtime/config/versioned-document.d.ts.map +1 -0
- package/dist/runtime/config/versioned-document.js +87 -0
- package/dist/runtime/config/versioned-document.js.map +1 -0
- package/dist/runtime/directory-changes.d.ts +12 -0
- package/dist/runtime/directory-changes.d.ts.map +1 -0
- package/dist/runtime/directory-changes.js +62 -0
- package/dist/runtime/directory-changes.js.map +1 -0
- package/dist/runtime/foreground.d.ts +2 -0
- package/dist/runtime/foreground.d.ts.map +1 -1
- package/dist/runtime/foreground.js +5 -20
- package/dist/runtime/foreground.js.map +1 -1
- package/dist/runtime/infra/infra-runner.js +5 -5
- package/dist/runtime/infra/infra-runner.js.map +1 -1
- package/dist/runtime/infra/keycloak-realm.d.ts +3 -19
- package/dist/runtime/infra/keycloak-realm.d.ts.map +1 -1
- package/dist/runtime/infra/keycloak-realm.js +12 -133
- package/dist/runtime/infra/keycloak-realm.js.map +1 -1
- package/dist/runtime/installation/current-launcher-renderer.js.map +1 -1
- package/dist/runtime/installation/from-current-config.d.ts.map +1 -1
- package/dist/runtime/installation/from-current-config.js +24 -47
- package/dist/runtime/installation/from-current-config.js.map +1 -1
- package/dist/runtime/installation/index.d.ts +1 -0
- package/dist/runtime/installation/index.d.ts.map +1 -1
- package/dist/runtime/installation/index.js +1 -0
- package/dist/runtime/installation/index.js.map +1 -1
- package/dist/runtime/installation/installation-file.d.ts +3 -0
- package/dist/runtime/installation/installation-file.d.ts.map +1 -0
- package/dist/runtime/installation/installation-file.js +9 -0
- package/dist/runtime/installation/installation-file.js.map +1 -0
- package/dist/runtime/installation/readiness.d.ts +6 -7
- package/dist/runtime/installation/readiness.d.ts.map +1 -1
- package/dist/runtime/installation/readiness.js +82 -132
- package/dist/runtime/installation/readiness.js.map +1 -1
- package/dist/runtime/installation/state-store.d.ts +4 -0
- package/dist/runtime/installation/state-store.d.ts.map +1 -1
- package/dist/runtime/installation/state-store.js +22 -1
- package/dist/runtime/installation/state-store.js.map +1 -1
- package/dist/runtime/installation/to-current-config.d.ts +4 -1
- package/dist/runtime/installation/to-current-config.d.ts.map +1 -1
- package/dist/runtime/installation/to-current-config.js +10 -36
- package/dist/runtime/installation/to-current-config.js.map +1 -1
- package/dist/runtime/launch-plan-format.d.ts.map +1 -1
- package/dist/runtime/launch-plan-format.js +31 -6
- package/dist/runtime/launch-plan-format.js.map +1 -1
- package/dist/runtime/launch-result.d.ts +15 -0
- package/dist/runtime/launch-result.d.ts.map +1 -0
- package/dist/runtime/launch-result.js +3 -0
- package/dist/runtime/launch-result.js.map +1 -0
- package/dist/runtime/launch.d.ts +16 -5
- package/dist/runtime/launch.d.ts.map +1 -1
- package/dist/runtime/launch.js +151 -315
- package/dist/runtime/launch.js.map +1 -1
- package/dist/runtime/license/helm-pull-secret.d.ts +1 -1
- package/dist/runtime/license/helm-pull-secret.d.ts.map +1 -1
- package/dist/runtime/license/helm-pull-secret.js +2 -2
- package/dist/runtime/license/helm-pull-secret.js.map +1 -1
- package/dist/runtime/lifecycle-progress/index.d.ts +9 -0
- package/dist/runtime/lifecycle-progress/index.d.ts.map +1 -0
- package/dist/runtime/lifecycle-progress/index.js +30 -0
- package/dist/runtime/lifecycle-progress/index.js.map +1 -0
- package/dist/runtime/lifecycle-progress/json-renderer.d.ts +3 -0
- package/dist/runtime/lifecycle-progress/json-renderer.d.ts.map +1 -0
- package/dist/runtime/lifecycle-progress/json-renderer.js +27 -0
- package/dist/runtime/lifecycle-progress/json-renderer.js.map +1 -0
- package/dist/runtime/lifecycle-progress/text-renderer.d.ts +5 -0
- package/dist/runtime/lifecycle-progress/text-renderer.d.ts.map +1 -0
- package/dist/runtime/lifecycle-progress/text-renderer.js +83 -0
- package/dist/runtime/lifecycle-progress/text-renderer.js.map +1 -0
- package/dist/runtime/lifecycle-progress/types.d.ts +79 -0
- package/dist/runtime/lifecycle-progress/types.d.ts.map +1 -0
- package/dist/runtime/lifecycle-progress/types.js +32 -0
- package/dist/runtime/lifecycle-progress/types.js.map +1 -0
- package/dist/runtime/lock-loader.d.ts +13 -3
- package/dist/runtime/lock-loader.d.ts.map +1 -1
- package/dist/runtime/lock-loader.js +28 -8
- package/dist/runtime/lock-loader.js.map +1 -1
- package/dist/runtime/paths.d.ts +2 -0
- package/dist/runtime/paths.d.ts.map +1 -1
- package/dist/runtime/paths.js +17 -4
- package/dist/runtime/paths.js.map +1 -1
- package/dist/runtime/roster.d.ts +6 -5
- package/dist/runtime/roster.d.ts.map +1 -1
- package/dist/runtime/roster.js +0 -21
- package/dist/runtime/roster.js.map +1 -1
- package/dist/runtime/runner/container-preflight.d.ts +3 -2
- package/dist/runtime/runner/container-preflight.d.ts.map +1 -1
- package/dist/runtime/runner/container-preflight.js +5 -5
- package/dist/runtime/runner/container-preflight.js.map +1 -1
- package/dist/runtime/runner/container-runner.d.ts +0 -22
- package/dist/runtime/runner/container-runner.d.ts.map +1 -1
- package/dist/runtime/runner/container-runner.js +0 -112
- package/dist/runtime/runner/container-runner.js.map +1 -1
- package/dist/runtime/runner/docker-cli.d.ts +1 -0
- package/dist/runtime/runner/docker-cli.d.ts.map +1 -1
- package/dist/runtime/runner/docker-cli.js +19 -0
- package/dist/runtime/runner/docker-cli.js.map +1 -1
- package/dist/runtime/runner/helm-cli.d.ts +1 -0
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js +5 -1
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/runner/helm-pre-install.d.ts +3 -2
- package/dist/runtime/runner/helm-pre-install.d.ts.map +1 -1
- package/dist/runtime/runner/helm-pre-install.js +3 -3
- package/dist/runtime/runner/helm-pre-install.js.map +1 -1
- package/dist/runtime/runner/helm-runner.d.ts +29 -4
- package/dist/runtime/runner/helm-runner.d.ts.map +1 -1
- package/dist/runtime/runner/helm-runner.js +44 -14
- package/dist/runtime/runner/helm-runner.js.map +1 -1
- package/dist/runtime/runner/kube-cli.d.ts +1 -0
- package/dist/runtime/runner/kube-cli.d.ts.map +1 -1
- package/dist/runtime/runner/kube-cli.js +24 -2
- package/dist/runtime/runner/kube-cli.js.map +1 -1
- package/dist/runtime/secrets/managed-local-key-provider.d.ts +2 -0
- package/dist/runtime/secrets/managed-local-key-provider.d.ts.map +1 -1
- package/dist/runtime/secrets/managed-local-key-provider.js +17 -8
- package/dist/runtime/secrets/managed-local-key-provider.js.map +1 -1
- package/dist/runtime/seed/closure-container.d.ts +8 -0
- package/dist/runtime/seed/closure-container.d.ts.map +1 -0
- package/dist/runtime/seed/closure-container.js +23 -0
- package/dist/runtime/seed/closure-container.js.map +1 -0
- package/dist/runtime/seed/closure-helm.d.ts +9 -0
- package/dist/runtime/seed/closure-helm.d.ts.map +1 -0
- package/dist/runtime/seed/closure-helm.js +16 -0
- package/dist/runtime/seed/closure-helm.js.map +1 -0
- package/dist/runtime/seed/closure-names.d.ts +4 -0
- package/dist/runtime/seed/closure-names.d.ts.map +1 -0
- package/dist/runtime/seed/closure-names.js +29 -0
- package/dist/runtime/seed/closure-names.js.map +1 -0
- package/dist/runtime/seed/closure-native.d.ts +3 -0
- package/dist/runtime/seed/closure-native.d.ts.map +1 -0
- package/dist/runtime/seed/closure-native.js +18 -0
- package/dist/runtime/seed/closure-native.js.map +1 -0
- package/dist/runtime/seed/installation-tar.d.ts +5 -0
- package/dist/runtime/seed/installation-tar.d.ts.map +1 -0
- package/dist/runtime/seed/installation-tar.js +30 -0
- package/dist/runtime/seed/installation-tar.js.map +1 -0
- package/dist/runtime/seed/launch-env.d.ts +42 -0
- package/dist/runtime/seed/launch-env.d.ts.map +1 -0
- package/dist/runtime/seed/launch-env.js +150 -0
- package/dist/runtime/seed/launch-env.js.map +1 -0
- package/dist/runtime/seed/seed-container.d.ts +13 -0
- package/dist/runtime/seed/seed-container.d.ts.map +1 -0
- package/dist/runtime/seed/seed-container.js +175 -0
- package/dist/runtime/seed/seed-container.js.map +1 -0
- package/dist/runtime/seed/seed-delivery-env.d.ts +39 -0
- package/dist/runtime/seed/seed-delivery-env.d.ts.map +1 -0
- package/dist/runtime/seed/seed-delivery-env.js +47 -0
- package/dist/runtime/seed/seed-delivery-env.js.map +1 -0
- package/dist/runtime/seed/seed-down.d.ts +2 -0
- package/dist/runtime/seed/seed-down.d.ts.map +1 -0
- package/dist/runtime/seed/seed-down.js +52 -0
- package/dist/runtime/seed/seed-down.js.map +1 -0
- package/dist/runtime/seed/seed-env-file.d.ts +3 -0
- package/dist/runtime/seed/seed-env-file.d.ts.map +1 -0
- package/dist/runtime/seed/seed-env-file.js +24 -0
- package/dist/runtime/seed/seed-env-file.js.map +1 -0
- package/dist/runtime/seed/seed-env-secret.d.ts +18 -0
- package/dist/runtime/seed/seed-env-secret.d.ts.map +1 -0
- package/dist/runtime/seed/seed-env-secret.js +62 -0
- package/dist/runtime/seed/seed-env-secret.js.map +1 -0
- package/dist/runtime/seed/seed-environment.d.ts +12 -0
- package/dist/runtime/seed/seed-environment.d.ts.map +1 -0
- package/dist/runtime/seed/seed-environment.js +66 -0
- package/dist/runtime/seed/seed-environment.js.map +1 -0
- package/dist/runtime/seed/seed-facts.d.ts +22 -0
- package/dist/runtime/seed/seed-facts.d.ts.map +1 -0
- package/dist/runtime/seed/seed-facts.js +40 -0
- package/dist/runtime/seed/seed-facts.js.map +1 -0
- package/dist/runtime/seed/seed-helm.d.ts +10 -0
- package/dist/runtime/seed/seed-helm.d.ts.map +1 -0
- package/dist/runtime/seed/seed-helm.js +74 -0
- package/dist/runtime/seed/seed-helm.js.map +1 -0
- package/dist/runtime/seed/seed-image-user.d.ts +6 -0
- package/dist/runtime/seed/seed-image-user.d.ts.map +1 -0
- package/dist/runtime/seed/seed-image-user.js +14 -0
- package/dist/runtime/seed/seed-image-user.js.map +1 -0
- package/dist/runtime/seed/seed-installation-files.d.ts +23 -0
- package/dist/runtime/seed/seed-installation-files.d.ts.map +1 -0
- package/dist/runtime/seed/seed-installation-files.js +51 -0
- package/dist/runtime/seed/seed-installation-files.js.map +1 -0
- package/dist/runtime/seed/seed-installation.d.ts +20 -0
- package/dist/runtime/seed/seed-installation.d.ts.map +1 -0
- package/dist/runtime/seed/seed-installation.js +36 -0
- package/dist/runtime/seed/seed-installation.js.map +1 -0
- package/dist/runtime/seed/seed-launcher.d.ts +14 -0
- package/dist/runtime/seed/seed-launcher.d.ts.map +1 -0
- package/dist/runtime/seed/seed-launcher.js +155 -0
- package/dist/runtime/seed/seed-launcher.js.map +1 -0
- package/dist/runtime/seed/seed-options.d.ts +22 -0
- package/dist/runtime/seed/seed-options.d.ts.map +1 -0
- package/dist/runtime/seed/seed-options.js +57 -0
- package/dist/runtime/seed/seed-options.js.map +1 -0
- package/dist/runtime/seed/seed-platform-endpoints.d.ts +8 -0
- package/dist/runtime/seed/seed-platform-endpoints.d.ts.map +1 -0
- package/dist/runtime/seed/seed-platform-endpoints.js +55 -0
- package/dist/runtime/seed/seed-platform-endpoints.js.map +1 -0
- package/dist/runtime/seed/seed-process.d.ts +22 -0
- package/dist/runtime/seed/seed-process.d.ts.map +1 -0
- package/dist/runtime/seed/seed-process.js +79 -0
- package/dist/runtime/seed/seed-process.js.map +1 -0
- package/dist/runtime/seed/seed-record.d.ts +101 -0
- package/dist/runtime/seed/seed-record.d.ts.map +1 -0
- package/dist/runtime/seed/seed-record.js +141 -0
- package/dist/runtime/seed/seed-record.js.map +1 -0
- package/dist/runtime/seed/seed-resolved-deployment.d.ts +14 -0
- package/dist/runtime/seed/seed-resolved-deployment.d.ts.map +1 -0
- package/dist/runtime/seed/seed-resolved-deployment.js +52 -0
- package/dist/runtime/seed/seed-resolved-deployment.js.map +1 -0
- package/dist/runtime/seed/substrate-facts.d.ts +52 -0
- package/dist/runtime/seed/substrate-facts.d.ts.map +1 -0
- package/dist/runtime/seed/substrate-facts.js +92 -0
- package/dist/runtime/seed/substrate-facts.js.map +1 -0
- package/dist/runtime/seed/supervisor-contract.d.ts +9 -0
- package/dist/runtime/seed/supervisor-contract.d.ts.map +1 -0
- package/dist/runtime/seed/supervisor-contract.js +68 -0
- package/dist/runtime/seed/supervisor-contract.js.map +1 -0
- package/dist/runtime/seed/workload-identity-facts.d.ts +17 -0
- package/dist/runtime/seed/workload-identity-facts.d.ts.map +1 -0
- package/dist/runtime/seed/workload-identity-facts.js +53 -0
- package/dist/runtime/seed/workload-identity-facts.js.map +1 -0
- package/dist/runtime/sources/schema.d.ts +2 -0
- package/dist/runtime/sources/schema.d.ts.map +1 -1
- package/dist/runtime/sources/schema.js +10 -9
- package/dist/runtime/sources/schema.js.map +1 -1
- package/dist/runtime/store-client.d.ts +0 -10
- package/dist/runtime/store-client.d.ts.map +1 -1
- package/dist/runtime/store-client.js +0 -5
- package/dist/runtime/store-client.js.map +1 -1
- package/dist/runtime/tenancy/identity-installer.d.ts +11 -0
- package/dist/runtime/tenancy/identity-installer.d.ts.map +1 -0
- package/dist/runtime/tenancy/identity-installer.js +39 -0
- package/dist/runtime/tenancy/identity-installer.js.map +1 -0
- package/dist/runtime/tenancy/import-cache.d.ts +5 -0
- package/dist/runtime/tenancy/import-cache.d.ts.map +1 -0
- package/dist/runtime/tenancy/import-cache.js +27 -0
- package/dist/runtime/tenancy/import-cache.js.map +1 -0
- package/dist/runtime/tenancy/openfga.d.ts +4 -0
- package/dist/runtime/tenancy/openfga.d.ts.map +1 -0
- package/dist/runtime/tenancy/openfga.js +29 -0
- package/dist/runtime/tenancy/openfga.js.map +1 -0
- package/dist/runtime/tenancy/servers.d.ts +18 -0
- package/dist/runtime/tenancy/servers.d.ts.map +1 -0
- package/dist/runtime/tenancy/servers.js +102 -0
- package/dist/runtime/tenancy/servers.js.map +1 -0
- package/dist/runtime/tenancy/session.d.ts +19 -0
- package/dist/runtime/tenancy/session.d.ts.map +1 -0
- package/dist/runtime/tenancy/session.js +79 -0
- package/dist/runtime/tenancy/session.js.map +1 -0
- package/dist/runtime/tenancy/specs.d.ts +27 -0
- package/dist/runtime/tenancy/specs.d.ts.map +1 -0
- package/dist/runtime/tenancy/specs.js +129 -0
- package/dist/runtime/tenancy/specs.js.map +1 -0
- package/examples/01-local-native.yaml +1 -3
- package/examples/02-local-container-full.yaml +0 -2
- package/examples/03-byo-infra.yaml +1 -5
- package/examples/06-biome-sources.yaml +1 -3
- package/package.json +32 -37
- package/xema.config.example.yaml +1 -10
- package/dist/assets/keycloak/realm-xema.json +0 -397
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js +0 -196
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/edition.d.ts +0 -3
- package/dist/commands/edition.d.ts.map +0 -1
- package/dist/commands/edition.js +0 -54
- package/dist/commands/edition.js.map +0 -1
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/serve.js +0 -114
- package/dist/commands/serve.js.map +0 -1
- package/dist/dev/carved-installs.d.ts +0 -3
- package/dist/dev/carved-installs.d.ts.map +0 -1
- package/dist/dev/carved-installs.js +0 -63
- package/dist/dev/carved-installs.js.map +0 -1
- package/dist/dev/env-defaults.d.ts +0 -15
- package/dist/dev/env-defaults.d.ts.map +0 -1
- package/dist/dev/env-defaults.js +0 -178
- package/dist/dev/env-defaults.js.map +0 -1
- package/dist/dev/forked-worker-bootstrap-protocol.d.ts +0 -17
- package/dist/dev/forked-worker-bootstrap-protocol.d.ts.map +0 -1
- package/dist/dev/forked-worker-bootstrap-protocol.js +0 -35
- package/dist/dev/forked-worker-bootstrap-protocol.js.map +0 -1
- package/dist/dev/forked-worker-bootstrap.d.ts +0 -18
- package/dist/dev/forked-worker-bootstrap.d.ts.map +0 -1
- package/dist/dev/forked-worker-bootstrap.js +0 -84
- package/dist/dev/forked-worker-bootstrap.js.map +0 -1
- package/dist/dev/forked-worker.d.ts +0 -43
- package/dist/dev/forked-worker.d.ts.map +0 -1
- package/dist/dev/forked-worker.js +0 -358
- package/dist/dev/forked-worker.js.map +0 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +0 -70456
- package/dist/edition/verify-image-evidence.bundle.provenance.json +0 -3957
- package/dist/runtime/backing-registration.d.ts +0 -11
- package/dist/runtime/backing-registration.d.ts.map +0 -1
- package/dist/runtime/backing-registration.js +0 -94
- package/dist/runtime/backing-registration.js.map +0 -1
- package/dist/runtime/edition/run-entry.d.ts +0 -9
- package/dist/runtime/edition/run-entry.d.ts.map +0 -1
- package/dist/runtime/edition/run-entry.js +0 -36
- package/dist/runtime/edition/run-entry.js.map +0 -1
- package/dist/runtime/edition/verb-map.d.ts +0 -70
- package/dist/runtime/edition/verb-map.d.ts.map +0 -1
- package/dist/runtime/edition/verb-map.js +0 -166
- package/dist/runtime/edition/verb-map.js.map +0 -1
- package/dist/runtime/env-seeder.d.ts +0 -26
- package/dist/runtime/env-seeder.d.ts.map +0 -1
- package/dist/runtime/env-seeder.js +0 -340
- package/dist/runtime/env-seeder.js.map +0 -1
- package/dist/runtime/port-map.d.ts +0 -3
- package/dist/runtime/port-map.d.ts.map +0 -1
- package/dist/runtime/port-map.js +0 -25
- package/dist/runtime/port-map.js.map +0 -1
- package/dist/runtime/run-manifest.d.ts +0 -52
- package/dist/runtime/run-manifest.d.ts.map +0 -1
- package/dist/runtime/run-manifest.js +0 -585
- package/dist/runtime/run-manifest.js.map +0 -1
- package/dist/runtime/runner/native-runner.d.ts +0 -13
- package/dist/runtime/runner/native-runner.d.ts.map +0 -1
- package/dist/runtime/runner/native-runner.js +0 -94
- package/dist/runtime/runner/native-runner.js.map +0 -1
- package/dist/runtime/runner/service-runner.d.ts +0 -28
- package/dist/runtime/runner/service-runner.d.ts.map +0 -1
- package/dist/runtime/runner/service-runner.js +0 -10
- package/dist/runtime/runner/service-runner.js.map +0 -1
- package/dist/runtime/secrets/generated-platform-secrets.d.ts +0 -16
- package/dist/runtime/secrets/generated-platform-secrets.d.ts.map +0 -1
- package/dist/runtime/secrets/generated-platform-secrets.js +0 -115
- package/dist/runtime/secrets/generated-platform-secrets.js.map +0 -1
- package/dist/runtime/secrets/resolver.d.ts +0 -23
- package/dist/runtime/secrets/resolver.d.ts.map +0 -1
- package/dist/runtime/secrets/resolver.js +0 -266
- package/dist/runtime/secrets/resolver.js.map +0 -1
- package/dist/runtime/supervisor.d.ts +0 -67
- package/dist/runtime/supervisor.d.ts.map +0 -1
- package/dist/runtime/supervisor.js +0 -227
- package/dist/runtime/supervisor.js.map +0 -1
- package/edition/README.md +0 -293
- package/edition/adapter-bundled-versions.mjs +0 -108
- package/edition/adapter-host-versions.mjs +0 -41
- package/edition/assemble-producer-input.mjs +0 -1423
- package/edition/assemble-third-party-input.mjs +0 -336
- package/edition/authored-source-check.mjs +0 -135
- package/edition/authorization-base.mjs +0 -357
- package/edition/biome-content.mjs +0 -327
- package/edition/build-biome-index.mjs +0 -180
- package/edition/build-ledger-completeness.mjs +0 -67
- package/edition/build-ledger.mjs +0 -107
- package/edition/build-platform-service-index.mjs +0 -292
- package/edition/build-runtime-image-source-index.mjs +0 -1187
- package/edition/built-verifier.mjs +0 -55
- package/edition/candidate-edition.mjs +0 -697
- package/edition/canonical-evidence-json.mjs +0 -82
- package/edition/canonical-repository-url.mjs +0 -47
- package/edition/catalogue-edition.mjs +0 -49
- package/edition/catalogue-evidence.mjs +0 -228
- package/edition/catalogue-fetch.mjs +0 -124
- package/edition/catalogue-lock-field.mjs +0 -41
- package/edition/catalogue-pin.mjs +0 -71
- package/edition/catalogue.mjs +0 -223
- package/edition/chart-set-source-commit.mjs +0 -97
- package/edition/check-committed-image-evidence.mjs +0 -412
- package/edition/checkout-indexed-source-repositories.mjs +0 -283
- package/edition/collected-refusals.mjs +0 -59
- package/edition/comment-blanker.mjs +0 -75
- package/edition/connector-source-authority.mjs +0 -60
- package/edition/contract-authority.mjs +0 -122
- package/edition/cosign-certificate-constraints.mjs +0 -50
- package/edition/cross-repo-build-inputs.mjs +0 -158
- package/edition/customer-definition.mjs +0 -174
- package/edition/customer-edition-verbs.mjs +0 -617
- package/edition/customer-flags.mjs +0 -15
- package/edition/develop-catalogue-free-resolve.mjs +0 -280
- package/edition/develop-chart-roster.mjs +0 -96
- package/edition/develop-chart-set.mjs +0 -147
- package/edition/develop-coverage-readers.mjs +0 -217
- package/edition/develop-coverage-run.mjs +0 -50
- package/edition/develop-coverage-verdict.mjs +0 -153
- package/edition/develop-coverage.mjs +0 -131
- package/edition/develop-current-resolution.mjs +0 -29
- package/edition/develop-edition-invariants.mjs +0 -142
- package/edition/develop-edition-resolve.mjs +0 -211
- package/edition/develop-edition.mjs +0 -379
- package/edition/develop-engine-image.mjs +0 -95
- package/edition/develop-follow.mjs +0 -491
- package/edition/develop-freeze.mjs +0 -289
- package/edition/develop-git-bundles.mjs +0 -255
- package/edition/develop-graph.mjs +0 -28
- package/edition/develop-image-coordinate.mjs +0 -54
- package/edition/develop-image-probe.mjs +0 -120
- package/edition/develop-image-record.mjs +0 -194
- package/edition/develop-record-ancestry.mjs +0 -144
- package/edition/develop-record-verify.mjs +0 -216
- package/edition/develop-resolve-report.mjs +0 -159
- package/edition/develop-select-records.mjs +0 -172
- package/edition/develop-selection-line.mjs +0 -30
- package/edition/develop-selection-pins.mjs +0 -36
- package/edition/develop-snapshot-inputs.mjs +0 -93
- package/edition/develop-verify-image-record.mjs +0 -109
- package/edition/dispatched-runtime-components.mjs +0 -172
- package/edition/dist-check.mjs +0 -1137
- package/edition/distribution-evidence-set.mjs +0 -1064
- package/edition/distribution-paths.mjs +0 -191
- package/edition/edition-artifact.mjs +0 -529
- package/edition/edition-biome-manifests.mjs +0 -85
- package/edition/edition-ceiling.mjs +0 -180
- package/edition/edition-channel.mjs +0 -20
- package/edition/edition-chart-set.mjs +0 -104
- package/edition/edition-config.mjs +0 -374
- package/edition/edition-constants.mjs +0 -110
- package/edition/edition-contract-parse.mjs +0 -179
- package/edition/edition-contract.json +0 -100
- package/edition/edition-image-seal.mjs +0 -110
- package/edition/edition-kind.mjs +0 -10
- package/edition/edition-list.mjs +0 -35
- package/edition/edition-locks.mjs +0 -79
- package/edition/edition-pin-verbs.mjs +0 -183
- package/edition/edition-pin.mjs +0 -17
- package/edition/edition-pinned-inputs.mjs +0 -116
- package/edition/edition-protocol.mjs +0 -277
- package/edition/edition-publisher.mjs +0 -21
- package/edition/edition-seal.mjs +0 -257
- package/edition/edition-source-pins.mjs +0 -211
- package/edition/edition-unpin.mjs +0 -16
- package/edition/edition-workspace.mjs +0 -204
- package/edition/evidence-checkout.mjs +0 -439
- package/edition/evidence-grammars.mjs +0 -18
- package/edition/evidence-input-mode.mjs +0 -136
- package/edition/evidence-trust-pin.mjs +0 -380
- package/edition/evidence-trust-policy.mjs +0 -374
- package/edition/external-source-walk.mjs +0 -485
- package/edition/first-party-source-ids.mjs +0 -62
- package/edition/fleet-coordinate-finality.mjs +0 -363
- package/edition/fulcio-certificate-extensions.mjs +0 -231
- package/edition/git-json.mjs +0 -54
- package/edition/git-source-auth.mjs +0 -131
- package/edition/image-delivery-entitlement-descriptor.mjs +0 -106
- package/edition/image-evidence-replay-state.mjs +0 -242
- package/edition/image-evidence.mjs +0 -1404
- package/edition/image-lock-service-rows.mjs +0 -26
- package/edition/image-subject-identity.mjs +0 -24
- package/edition/images-from-record.mjs +0 -298
- package/edition/indexes-at-pins.mjs +0 -13
- package/edition/kernel-capability-domain.mjs +0 -30
- package/edition/kernel-contract-authority.mjs +0 -517
- package/edition/kernel-contract-source-path-set.mjs +0 -94
- package/edition/kernel-package.mjs +0 -206
- package/edition/kernel-protocol-choice.mjs +0 -239
- package/edition/kernel-signer-kind.mjs +0 -23
- package/edition/lib.mjs +0 -3303
- package/edition/locked-bundle-signer.mjs +0 -297
- package/edition/map-with-concurrency.mjs +0 -19
- package/edition/no-follow-file.mjs +0 -94
- package/edition/oci-evidence-verbs.mjs +0 -419
- package/edition/oci-image-source-evidence.mjs +0 -364
- package/edition/operation-grant-admission.mjs +0 -285
- package/edition/operation-requirements.mjs +0 -282
- package/edition/oras-process.mjs +0 -35
- package/edition/oras.mjs +0 -742
- package/edition/pin-source.mjs +0 -242
- package/edition/pinned-cosign.mjs +0 -70
- package/edition/plan-image-evidence-refresh.mjs +0 -427
- package/edition/platform-lock-readability.mjs +0 -901
- package/edition/platform-service-class.mjs +0 -85
- package/edition/platform-service-ownership.mjs +0 -88
- package/edition/platform-service-source-authority.mjs +0 -105
- package/edition/positive-closure.mjs +0 -162
- package/edition/previous-seal-registry.mjs +0 -149
- package/edition/produce-distribution-completeness-evidence.mjs +0 -318
- package/edition/produce-edition-catalogue-evidence.mjs +0 -252
- package/edition/produce-image-delivery-entitlement-evidence.mjs +0 -304
- package/edition/produce-image-source-evidence.mjs +0 -788
- package/edition/producer-evidence-bundle.mjs +0 -201
- package/edition/producer-evidence-index-file.mjs +0 -4
- package/edition/record-refused.mjs +0 -12
- package/edition/registry-credentials.mjs +0 -225
- package/edition/release-catalogue.mjs +0 -47
- package/edition/release-channel-guard.mjs +0 -108
- package/edition/release-edition-artifact.mjs +0 -152
- package/edition/release-edition.mjs +0 -54
- package/edition/remedy-command.mjs +0 -89
- package/edition/repin-evidence-trust.mjs +0 -109
- package/edition/required-resource-provider.mjs +0 -58
- package/edition/resolve-chart-set.mjs +0 -376
- package/edition/resolve-customer-edition.mjs +0 -374
- package/edition/resolve-distribution.mjs +0 -2639
- package/edition/resolve-edition.mjs +0 -192
- package/edition/resolve-images.mjs +0 -2351
- package/edition/seal-image-digests.mjs +0 -954
- package/edition/sealed-distribution-lock.mjs +0 -22
- package/edition/sealed-evidence-accept.mjs +0 -82
- package/edition/shallow-checkout.mjs +0 -18
- package/edition/sigstore-bundle-media-type.mjs +0 -4
- package/edition/source-authority.mjs +0 -713
- package/edition/source-closure.mjs +0 -115
- package/edition/store-publisher-trust.mjs +0 -40
- package/edition/strict-json.mjs +0 -121
- package/edition/string-order.mjs +0 -5
- package/edition/third-party-admission.mjs +0 -91
- package/edition/third-party-bundle-registry.mjs +0 -242
- package/edition/third-party-certificate-extensions.mjs +0 -21
- package/edition/third-party-image-pins.mjs +0 -41
- package/edition/third-party-pins-trusted.mjs +0 -24
- package/edition/third-party-producer-evidence.mjs +0 -76
- package/edition/third-party-source-check.mjs +0 -28
- package/edition/third-party-source-context.mjs +0 -41
- package/edition/third-party-source-evidence.mjs +0 -287
- package/edition/third-party-source-flags.mjs +0 -134
- package/edition/third-party-source-pins.mjs +0 -259
- package/edition/third-party-source-uniqueness.mjs +0 -54
- package/edition/third-party-source-walk.mjs +0 -115
- package/edition/third-party-source-writer.mjs +0 -213
- package/edition/third-party-subjects.mjs +0 -128
- package/edition/trust-policy-arming-verb.mjs +0 -60
- package/edition/trust-policy-arming.mjs +0 -144
- package/edition/trust-policy-image-ownership.mjs +0 -85
- package/edition/trusted-executable.mjs +0 -166
- package/edition/verifier-binding.mjs +0 -45
- package/edition/verifier-files.mjs +0 -8
- package/edition/verifier-kernel-schemas.mjs +0 -22
- package/edition/verifier-provenance.mjs +0 -64
- package/edition/verify-catalogue-evidence.mjs +0 -247
- package/edition/verify-image-evidence.mjs +0 -475
- package/edition/verify-image-platforms.mjs +0 -523
- package/edition/why.mjs +0 -56
- package/edition/workspace-sections.mjs +0 -276
|
@@ -1,2351 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Resolve a distribution into schema-v3 actual runtime image subjects.
|
|
3
|
-
// Services, the host shell, and independently sourced platform images are
|
|
4
|
-
// subjects. Biomes remain selection/ownership units and never become fictitious
|
|
5
|
-
// one-container-per-Biome build outputs.
|
|
6
|
-
//
|
|
7
|
-
// ── "REGENERATE AND DIFF" IS NOT A VALID CHECK ON THIS ARTIFACT ─────────────
|
|
8
|
-
//
|
|
9
|
-
// This resolver emits NO `imageDigest`. The digests in a committed
|
|
10
|
-
// `images.lock.json` are written afterwards, by `seal-image-digests.mjs`, and
|
|
11
|
-
// nothing here reproduces them. So the obvious verification — regenerate the
|
|
12
|
-
// lock and diff it against the committed one — reports drift that is purely the
|
|
13
|
-
// absence of a seal, on every subject, every time.
|
|
14
|
-
//
|
|
15
|
-
// Measured 2026-09-23 on `origin/main`, running this script unmodified against a
|
|
16
|
-
// pristine checkout with two editions left alone as controls:
|
|
17
|
-
//
|
|
18
|
-
// core 54 of 54 subjects carried a digest -> 0 after; `grep -c imageDigest` = 0
|
|
19
|
-
// cloud 66 of 66 -> 0 after
|
|
20
|
-
// minimal / oss (not re-resolved) -> 45 and 63, untouched
|
|
21
|
-
//
|
|
22
|
-
// The field is ABSENT rather than emptied, on `subjects`, `platformServiceBindings`
|
|
23
|
-
// and `webBiomes` alike, and no merge path carries a subset forward. Two
|
|
24
|
-
// consequences, and the second is the one that costs a night:
|
|
25
|
-
//
|
|
26
|
-
// - to verify a lock, compare a fresh resolve against ANOTHER fresh resolve
|
|
27
|
-
// (which cancels both the seal and the wall-clock `resolvedAt`), never
|
|
28
|
-
// against the committed file;
|
|
29
|
-
// - a re-resolve DISARMS the sealer's moved-tag refusal, which fires only when
|
|
30
|
-
// the lock being sealed already carries a digest for that subject. That is
|
|
31
|
-
// not a bypass to reach for — it is why "re-resolve then seal" and "re-seal
|
|
32
|
-
// the committed lock" are different operations with different verdicts, and
|
|
33
|
-
// anyone comparing their outcomes needs to know which one they ran.
|
|
34
|
-
|
|
35
|
-
import { execFileSync } from 'node:child_process';
|
|
36
|
-
import { createHash } from 'node:crypto';
|
|
37
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
38
|
-
import { join, relative, sep } from 'node:path';
|
|
39
|
-
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
40
|
-
|
|
41
|
-
import { assertBuildLedgerComplete } from './build-ledger-completeness.mjs';
|
|
42
|
-
import { BUILD_LEDGER_FILE, readBuildLedger } from './build-ledger.mjs';
|
|
43
|
-
import { CROSS_REPO_BUILD_INPUTS_PATH, crossRepoInputKey, resolveCrossRepoVendoredInputs } from './cross-repo-build-inputs.mjs';
|
|
44
|
-
import {
|
|
45
|
-
resolveDispatchedRuntimeComponents,
|
|
46
|
-
UndeclaredSupplementalRuntimeComponentError,
|
|
47
|
-
} from './dispatched-runtime-components.mjs';
|
|
48
|
-
import {
|
|
49
|
-
assertCanonicalDistributionId,
|
|
50
|
-
distributionOutputPath,
|
|
51
|
-
readBoundDistributionFile,
|
|
52
|
-
} from './distribution-paths.mjs';
|
|
53
|
-
import { editionWorkspaceConfig, workspaceSection } from './edition-config.mjs';
|
|
54
|
-
import { editionPinnedInputs } from './edition-pinned-inputs.mjs';
|
|
55
|
-
import { dataRoot, editionsRoot, WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, sourcePath, sourcesRoot } from './edition-workspace.mjs';
|
|
56
|
-
import {
|
|
57
|
-
externalSourceCacheDir,
|
|
58
|
-
materializeGitSource,
|
|
59
|
-
} from './external-source-walk.mjs';
|
|
60
|
-
import { assertDeclaredSourceId, firstPartySourceId } from './first-party-source-ids.mjs';
|
|
61
|
-
import {
|
|
62
|
-
compareStrings,
|
|
63
|
-
hashImageBuildInputs,
|
|
64
|
-
imageInputSourceHashAlgorithm,
|
|
65
|
-
imageLockWriteSchemaVersion,
|
|
66
|
-
loadKernelDistributionSchemas,
|
|
67
|
-
parseResolvedDistributionLock,
|
|
68
|
-
readGitJsonAtCommit,
|
|
69
|
-
serializeJson,
|
|
70
|
-
sha256Hex,
|
|
71
|
-
validateImmutableDistributionLock,
|
|
72
|
-
validateImmutableImageLock,
|
|
73
|
-
} from './lib.mjs';
|
|
74
|
-
import { assertReleaseContent } from './release-channel-guard.mjs';
|
|
75
|
-
import { composeEdition } from './resolve-distribution.mjs';
|
|
76
|
-
import { parseStrictJson, readStrictJsonFile } from './strict-json.mjs';
|
|
77
|
-
import { loadThirdPartySourcePins } from './third-party-source-pins.mjs';
|
|
78
|
-
import {
|
|
79
|
-
assertOciBundleSourceOrigin,
|
|
80
|
-
attestedContentHash,
|
|
81
|
-
isThirdPartyOciRow,
|
|
82
|
-
thirdPartyPinForRow,
|
|
83
|
-
thirdPartySubjectCandidates,
|
|
84
|
-
} from './third-party-subjects.mjs';
|
|
85
|
-
|
|
86
|
-
if (isEntryModule(import.meta.url)) {
|
|
87
|
-
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data, WorkspaceRoot.Editions, WorkspaceRoot.Sources, WorkspaceRoot.Kernel, WorkspaceRoot.Cache]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const firstPartyImageRegistry = () => editionWorkspaceConfig().imageRegistry;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* WHERE A LOCKED BIOME'S SOURCE TREE IS ON DISK — the one answer this plane
|
|
94
|
-
* needs, and the one thing every read in this file used to assume.
|
|
95
|
-
*
|
|
96
|
-
* Every Git read here was `sourcePath(<repo>)`, which encodes an
|
|
97
|
-
* assumption that held while every biome came from a sibling repository in the
|
|
98
|
-
* aggregator's layout: that a `repo` HANDLE IS A PATH under `sourcesRoot()`. A
|
|
99
|
-
* biome drawn from an EXTERNAL source breaks it — its `repo` is that source's
|
|
100
|
-
* own LOGICAL name (`sources[].name`), deliberately not a `repos/<x>` path,
|
|
101
|
-
* because a hosting repository's name is source-management detail and must not
|
|
102
|
-
* become a biome's provenance. `<sourcesRoot()>/<logical name>` is a directory that
|
|
103
|
-
* does not exist, so the content hash could not be computed at all.
|
|
104
|
-
*
|
|
105
|
-
* The fix is ONE locator rather than a special case per read site: a `repo`
|
|
106
|
-
* handle resolves to a checkout DIRECTORY plus the BASE its `path` is relative
|
|
107
|
-
* to. Both kinds answer the same shape — a first-party repo's paths are
|
|
108
|
-
* `sourcesRoot()`-relative, an external source's are relative to the cache the walk
|
|
109
|
-
* materialised it into — so every call site reads the same two fields and none
|
|
110
|
-
* of them knows which kind it has.
|
|
111
|
-
*
|
|
112
|
-
* WHAT THE HASH THEN COVERS IS UNCHANGED, and that is the point of routing the
|
|
113
|
-
* DIRECTORY rather than teaching the hash about sources. `hashImageBuildInputs`
|
|
114
|
-
* reads the service's own source tree, the transitive workspace-package
|
|
115
|
-
* closure, the exact Dockerfile blob the build resolves to, every file the
|
|
116
|
-
* Dockerfile copies out of the context, the filtered pnpm lock graph, the
|
|
117
|
-
* context-directory residue and any vendored cross-repo tree — all out of the
|
|
118
|
-
* Git objects of whatever repository it is handed, at an exact commit. Handing
|
|
119
|
-
* it an external checkout therefore covers that repository's FULL build input
|
|
120
|
-
* by the same construction it already covers a first-party one, with no second
|
|
121
|
-
* definition of what an image is built from.
|
|
122
|
-
*
|
|
123
|
-
* MATERIALISATION IS AT THE LOCKED COMMIT, never at the declared ref. The lock
|
|
124
|
-
* already recorded the exact commit its manifests were read at; re-resolving a
|
|
125
|
-
* ref here would let a hash be computed over a different revision than the one
|
|
126
|
-
* the edition locked, silently and in the direction nothing detects.
|
|
127
|
-
* `materializeGitSource` short-circuits an exact 40-hex ref instead of sending
|
|
128
|
-
* it to `ls-remote`, so passing the commit AS the ref is the pin rather than a
|
|
129
|
-
* re-resolution.
|
|
130
|
-
*/
|
|
131
|
-
export function externalSourceTrees(
|
|
132
|
-
lock,
|
|
133
|
-
{ cacheRoot = externalSourceCacheDir(), materialize = materializeGitSource } = {},
|
|
134
|
-
) {
|
|
135
|
-
const byRepo = new Map();
|
|
136
|
-
for (const biome of lock.biomes ?? []) {
|
|
137
|
-
if (biome.source === undefined) {continue;}
|
|
138
|
-
const name = biome.source.name;
|
|
139
|
-
const incumbent = byRepo.get(name);
|
|
140
|
-
if (incumbent) {
|
|
141
|
-
if (incumbent.commit !== biome.source.commit) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`distribution "${lock.distributionId}" pins two commits for external source ` +
|
|
144
|
-
`"${name}" (${incumbent.commit} and ${biome.source.commit}) — one source is one ` +
|
|
145
|
-
'revision, so an image built from it cannot be addressed',
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
byRepo.set(name, { source: biome.source, commit: biome.source.commit });
|
|
151
|
-
}
|
|
152
|
-
const trees = new Map();
|
|
153
|
-
for (const [name, { source, commit }] of byRepo) {
|
|
154
|
-
const { directory } = materialize({ source: { ...source, ref: commit }, cacheRoot });
|
|
155
|
-
trees.set(name, { dir: directory, base: cacheRoot });
|
|
156
|
-
}
|
|
157
|
-
return trees;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* `repo` handle -> `{ dir, base }`.
|
|
162
|
-
*
|
|
163
|
-
* An unknown handle is FIRST-PARTY, which is the only default that can be
|
|
164
|
-
* correct: the external set is CLOSED by the lock's own `sources[]`, so
|
|
165
|
-
* anything outside it came from this fleet's layout. A handle that is neither
|
|
166
|
-
* resolves to a directory that does not exist and fails at the first Git read,
|
|
167
|
-
* naming the path — a loud failure rather than a silent fall back to an empty
|
|
168
|
-
* tree, which would hash to a coordinate nothing ever published.
|
|
169
|
-
*/
|
|
170
|
-
export function sourceTreeLocator(externalTrees) {
|
|
171
|
-
return (repo) =>
|
|
172
|
-
externalTrees.get(repo) ?? { dir: sourcePath(repo), base: sourcesRoot() };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** The first-party-only locator — for callers that hold no lock to read. */
|
|
176
|
-
const FIRST_PARTY_TREES = (repo) => ({ dir: sourcePath(repo), base: sourcesRoot() });
|
|
177
|
-
const imageBuildInputHashByIdentity = new Map();
|
|
178
|
-
const sourceManifestHashByIdentity = new Map();
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Memoized image build-input closure hash.
|
|
182
|
-
*
|
|
183
|
-
* Every distribution that ships a service resolves the SAME closure for it, and
|
|
184
|
-
* the closure reads immutable Git objects at an exact commit, so one digest per
|
|
185
|
-
* (service, service path, commit, vendored inputs) is exact — not a cache with
|
|
186
|
-
* an invalidation story.
|
|
187
|
-
*/
|
|
188
|
-
function exactImageBuildInputHash(request, algorithm) {
|
|
189
|
-
const identity = serializeJson({ request, algorithm });
|
|
190
|
-
const cached = imageBuildInputHashByIdentity.get(identity);
|
|
191
|
-
if (cached) {return cached;}
|
|
192
|
-
const hash = hashImageBuildInputs(request, algorithm);
|
|
193
|
-
imageBuildInputHashByIdentity.set(identity, hash);
|
|
194
|
-
return hash;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function exactSourceManifestHash(
|
|
198
|
-
repoDirectory,
|
|
199
|
-
repoRelativePath,
|
|
200
|
-
sourceCommit,
|
|
201
|
-
) {
|
|
202
|
-
const identity = serializeJson({
|
|
203
|
-
repoDirectory,
|
|
204
|
-
repoRelativePath,
|
|
205
|
-
sourceCommit,
|
|
206
|
-
});
|
|
207
|
-
const cached = sourceManifestHashByIdentity.get(identity);
|
|
208
|
-
if (cached) {return cached;}
|
|
209
|
-
const hash = sha256Hex(
|
|
210
|
-
serializeJson(
|
|
211
|
-
readGitJsonAtCommit(repoDirectory, repoRelativePath, sourceCommit),
|
|
212
|
-
),
|
|
213
|
-
);
|
|
214
|
-
sourceManifestHashByIdentity.set(identity, hash);
|
|
215
|
-
return hash;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Key of one vendored input WITHIN one consumer repository.
|
|
220
|
-
*
|
|
221
|
-
* Both the store and the lookup go through this, so the two sides cannot spell
|
|
222
|
-
* the separator differently — which they did, and every lookup missed.
|
|
223
|
-
*/
|
|
224
|
-
function consumerInputKey(consumerRepo, input) {
|
|
225
|
-
return `${consumerRepo}\u0000${crossRepoInputKey(input)}`;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Compose one image ref: WHERE the artifact lives, and WHAT it is called.
|
|
230
|
-
*
|
|
231
|
-
* `registry` defaults to the vendor namespace because that is where everything
|
|
232
|
-
* this organisation BUILDS is published, and every caller but one wants it. The
|
|
233
|
-
* exception is a biome drawn from an external source, whose coordinate is a
|
|
234
|
-
* DECLARED fact carried on the lock (`biome.source.imageRegistry`) rather than a
|
|
235
|
-
* constant — see the biomes loop in `resolveImageStructure`.
|
|
236
|
-
*
|
|
237
|
-
* `namePrefix` is the second half of that same declared coordinate, and it is
|
|
238
|
-
* what keeps a subject's LOGICAL id separate from its PUBLISHED name. Those two
|
|
239
|
-
* are the same string for every first-party subject — measured 2026-09-15, 67
|
|
240
|
-
* of 67 deploy-manifest entries and 60 of 60 `oci-image` component keys — but
|
|
241
|
-
* that is a property of owning the whole of the first-party registry namespace, not a law. A
|
|
242
|
-
* customer organisation's namespace is flat and carries every product that
|
|
243
|
-
* customer ships, so a bare component name there identifies nothing in
|
|
244
|
-
* particular and the first collision silently overwrites somebody's tags.
|
|
245
|
-
*
|
|
246
|
-
* DEFAULTING TO THE EMPTY STRING IS THE IDENTITY ELEMENT OF CONCATENATION, not
|
|
247
|
-
* a fallback: an unqualified name is what every existing subject already gets,
|
|
248
|
-
* so the absent case reproduces today's behaviour exactly rather than
|
|
249
|
-
* substituting something. Contrast `registry`, whose absence on an external
|
|
250
|
-
* source is REFUSED in `resolveImageStructure` — there the only available
|
|
251
|
-
* default names the wrong ORG, which is a misdirection rather than a preference.
|
|
252
|
-
*
|
|
253
|
-
* Both parameters are deliberately not module-level mutables: two editions are
|
|
254
|
-
* resolved in one process by `dist:check`, and a coordinate held in module scope
|
|
255
|
-
* would leak one edition's customer namespace into the next one's subjects.
|
|
256
|
-
*/
|
|
257
|
-
export function imageRefForSubject(
|
|
258
|
-
subjectId,
|
|
259
|
-
registry = firstPartyImageRegistry(),
|
|
260
|
-
namePrefix = '',
|
|
261
|
-
) {
|
|
262
|
-
return `${registry}/${namePrefix}${subjectId}`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* The OCI platforms one distribution requires every runtime image to ship.
|
|
267
|
-
*
|
|
268
|
-
* Read from that edition's OWN `xema-distribution.json`
|
|
269
|
-
* and projected onto image-lock platform
|
|
270
|
-
* coordinates by the kernel. This deliberately replaced a global frozen
|
|
271
|
-
* `[{linux, amd64}]` constant: with one constant, every subject of every
|
|
272
|
-
* edition declared amd64 — including `appliance-lean`/`appliance-base`, whose
|
|
273
|
-
* installer (`xema-appliance/os/dgx/install-k3s.sh`, `ansible/site.yml`) refuses
|
|
274
|
-
* any host that is not ARM64. The image plane therefore declared the one
|
|
275
|
-
* architecture the appliance could not run, and nothing in the chain could say
|
|
276
|
-
* so.
|
|
277
|
-
*
|
|
278
|
-
* The kernel owns the vocabulary and the fail-fast: an absent, empty, unknown,
|
|
279
|
-
* duplicated or misordered declaration throws here rather than resolving to a
|
|
280
|
-
* default. There is no default.
|
|
281
|
-
*/
|
|
282
|
-
export function resolveDistributionRequiredPlatforms(schemas, id) {
|
|
283
|
-
assertCanonicalDistributionId(id);
|
|
284
|
-
const { requiredPlatforms } = composeEdition(schemas.parseDistribution, id);
|
|
285
|
-
return schemas.resolveRuntimeImagePlatforms(requiredPlatforms);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* The edition's DECLARED `sources[]`,
|
|
290
|
-
*
|
|
291
|
-
* WHY THIS IS A PARAMETER AND NOT A LOCK FIELD. `imageNamePrefix` is producer
|
|
292
|
-
* INPUT: it composes the image NAME and is then spent, because the lock carries
|
|
293
|
-
* the fully-qualified `imageRef`. It cannot travel in the lock to reach the
|
|
294
|
-
* resolver — `DistributionLockSchema` is `.strict()` and learned that key only
|
|
295
|
-
* in `@xemahq/contracts` 0.10.0, so a pinned image resolving `^0.9.x` refuses
|
|
296
|
-
* the whole lock at boot. Persisting it took a customer environment's three lock
|
|
297
|
-
* consumers into CrashLoopBackOff on 2026-09-17.
|
|
298
|
-
*
|
|
299
|
-
* So the producer reads its OWN declaration, and `requiredPlatforms` one
|
|
300
|
-
* function up is the precedent: a declared fact the pure structure resolver
|
|
301
|
-
* needs arrives as an argument the impure caller composes. That keeps
|
|
302
|
-
* `resolveImageStructure` pure over the committed lock and runtime index, which
|
|
303
|
-
* is what lets `dist-check` grade an images lock offline.
|
|
304
|
-
*/
|
|
305
|
-
export function resolveDistributionDeclaredSources(schemas, id) {
|
|
306
|
-
assertCanonicalDistributionId(id);
|
|
307
|
-
const { declaredSources } = composeEdition(schemas.parseDistribution, id);
|
|
308
|
-
return declaredSources;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function subjectKey(subject) {
|
|
312
|
-
return `${subject.kind}\0${subject.id}`;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function bindingKey(binding) {
|
|
316
|
-
return `${binding.subjectKind}\0${binding.subjectId}`;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function compareSubject(left, right) {
|
|
320
|
-
return (
|
|
321
|
-
compareStrings(left.kind, right.kind) || compareStrings(left.id, right.id)
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function compareOwner(left, right) {
|
|
326
|
-
return compareStrings(serializeJson(left), serializeJson(right));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function addSubject(subjects, candidate) {
|
|
330
|
-
const key = subjectKey(candidate);
|
|
331
|
-
const existing = subjects.get(key);
|
|
332
|
-
if (!existing) {
|
|
333
|
-
subjects.set(key, {
|
|
334
|
-
...candidate,
|
|
335
|
-
owners: [...candidate.owners],
|
|
336
|
-
});
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const withoutOwners = ({ owners: _owners, ...value }) => value;
|
|
340
|
-
if (
|
|
341
|
-
serializeJson(withoutOwners(existing)) !==
|
|
342
|
-
serializeJson(withoutOwners(candidate))
|
|
343
|
-
) {
|
|
344
|
-
throw new Error(
|
|
345
|
-
`runtime image subject "${candidate.kind}:${candidate.id}" has conflicting source identities`,
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
const ownerKeys = new Set(existing.owners.map(serializeJson));
|
|
349
|
-
for (const owner of candidate.owners) {
|
|
350
|
-
if (!ownerKeys.has(serializeJson(owner))) {existing.owners.push(owner);}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* The refusal for a SELECTED platform service whose declared artifact
|
|
356
|
-
* dependency no selected biome produces.
|
|
357
|
-
*
|
|
358
|
-
* The edge is DECLARED, never inferred from a name: a platform service's
|
|
359
|
-
* `runtimeBinding` (platform-service-source-authority.mjs, projected into
|
|
360
|
-
* `runtime-image-source-index.json#platformServiceBindings`) says which image
|
|
361
|
-
* subject carries it — `opencode-image-prepuller` → `runtime-component:
|
|
362
|
-
* opencode-worker`. When that subject is not supplemental it is produced by a
|
|
363
|
-
* biome, and the biome that produces it is the one whose OWN manifest declares
|
|
364
|
-
* it (`services` / `runtimeComponents` in the biome index). So excluding the
|
|
365
|
-
* provider biome while keeping the dependent service is refused here, naming
|
|
366
|
-
* both ends; excluding both leaves no edge and resolves.
|
|
367
|
-
*/
|
|
368
|
-
function missingArtifactDependencyMessage(lock, serviceName, binding, biomeIndex) {
|
|
369
|
-
const field =
|
|
370
|
-
binding.subjectKind === 'runtime-component' ? 'runtimeComponents' : 'services';
|
|
371
|
-
const providers = Array.isArray(biomeIndex)
|
|
372
|
-
? [
|
|
373
|
-
...new Set(
|
|
374
|
-
biomeIndex
|
|
375
|
-
.filter((entry) => (entry[field] ?? []).includes(binding.subjectId))
|
|
376
|
-
.map((entry) => entry.id),
|
|
377
|
-
),
|
|
378
|
-
].sort(compareStrings)
|
|
379
|
-
: [];
|
|
380
|
-
const providerText =
|
|
381
|
-
providers.length > 0
|
|
382
|
-
? `, which is declared by biome ${providers.map((id) => `"${id}"`).join(' / ')}` +
|
|
383
|
-
' — and this edition does not select it'
|
|
384
|
-
: ', and no biome this edition selects declares it';
|
|
385
|
-
return (
|
|
386
|
-
`distribution "${lock.distributionId}" ships platform service "${serviceName}", ` +
|
|
387
|
-
`whose declared runtime binding requires image subject ` +
|
|
388
|
-
`"${binding.subjectKind}:${binding.subjectId}"${providerText}. ` +
|
|
389
|
-
`Either keep the providing biome, or exclude "${serviceName}" as well — ` +
|
|
390
|
-
'refusing rather than resolving an edition whose service has no image.'
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Offline roster derivation. It reads only committed locks/indexes and performs
|
|
396
|
-
* no registry, GitHub, DigitalOcean, or source-worktree access.
|
|
397
|
-
*
|
|
398
|
-
* `requiredPlatforms` is supplied by the caller (see
|
|
399
|
-
* {@link resolveDistributionRequiredPlatforms}) rather than read here, so this
|
|
400
|
-
* function stays a pure projection of its inputs — and so a caller that forgets
|
|
401
|
-
* the distribution's declaration gets a hard error instead of a default.
|
|
402
|
-
*
|
|
403
|
-
* THE ROSTER IS THE ARTIFACT CLOSURE OF THE LOGICAL CLOSURE, and nothing else.
|
|
404
|
-
* `runtime-image-source-index.json` is the FLEET's catalogue — every platform
|
|
405
|
-
* service binding, every supplemental subject — and an edition selects from it.
|
|
406
|
-
* A binding for a platform service this edition does not ship is simply not
|
|
407
|
-
* part of this edition's artifact graph and is dropped; it is never a reason to
|
|
408
|
-
* refuse. What DOES refuse is a SELECTED service whose declared artifact
|
|
409
|
-
* dependency is absent from the closure (see `missingArtifactDependencyMessage`).
|
|
410
|
-
*
|
|
411
|
-
* `options.biomeIndex` is the first-party biome index. It names, in a refusal,
|
|
412
|
-
* which biome declares a missing artifact, and it carries each rostered biome's
|
|
413
|
-
* `dispatchesRuntimeComponents`: the runtime components that biome launches,
|
|
414
|
-
* whose images enter this roster as runtime-component rows although their
|
|
415
|
-
* biomes are not rostered (see `dispatched-runtime-components.mjs`).
|
|
416
|
-
*
|
|
417
|
-
* `options.supplementalSources` is the edition workspace's
|
|
418
|
-
* `runtimeImageSources.supplementalSources`: the declarations the source
|
|
419
|
-
* index's supplemental subjects were built from, carrying each one's
|
|
420
|
-
* `launchedBy`. A subject launched by a biome ships, owned by that biome, in
|
|
421
|
-
* every edition whose roster contains it; one a platform service binds ships
|
|
422
|
-
* with that service. A declared subject with neither is refused by name: it
|
|
423
|
-
* could never enter any edition, and it used to be dropped silently.
|
|
424
|
-
*/
|
|
425
|
-
export function resolveImageStructure(
|
|
426
|
-
lock,
|
|
427
|
-
runtimeSourceIndex,
|
|
428
|
-
enums,
|
|
429
|
-
requiredPlatforms,
|
|
430
|
-
declaredSources,
|
|
431
|
-
{ biomeIndex, supplementalSources, thirdPartyPins } = {},
|
|
432
|
-
) {
|
|
433
|
-
if (!Array.isArray(requiredPlatforms) || requiredPlatforms.length === 0) {
|
|
434
|
-
throw new Error(
|
|
435
|
-
`distribution "${lock.distributionId}" resolved no required image platforms — ` +
|
|
436
|
-
`every distribution declares requiredPlatforms in its xema-distribution.json ` +
|
|
437
|
-
`and there is no default; a silent default is the bug this replaced`,
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
if (!Array.isArray(biomeIndex)) {
|
|
441
|
-
throw new Error(
|
|
442
|
-
`distribution "${lock.distributionId}" resolved its images without the biome index — the index carries ` +
|
|
443
|
-
'the runtime components a rostered biome dispatches, so a roster without it can omit images',
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
if (!Array.isArray(supplementalSources)) {
|
|
447
|
-
throw new Error(
|
|
448
|
-
`distribution "${lock.distributionId}" resolved its images without the supplemental source declarations — ` +
|
|
449
|
-
'they say which biome launches a supplemental subject, so a roster without them can omit images',
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
const subjects = new Map();
|
|
453
|
-
// Third-party OCI subjects come from their pin and are never built, hashed or read from a checkout, so they are kept
|
|
454
|
-
// apart from every git-backed subject (see `third-party-subjects.mjs`).
|
|
455
|
-
const thirdPartySubjects = new Map();
|
|
456
|
-
const webBiomes = [];
|
|
457
|
-
const platformServiceBindings = [];
|
|
458
|
-
const repositoryPins = new Map(
|
|
459
|
-
runtimeSourceIndex.repositoryPins.map((repository) => [
|
|
460
|
-
repository.repo,
|
|
461
|
-
repository,
|
|
462
|
-
]),
|
|
463
|
-
);
|
|
464
|
-
const requireRepositoryPin = (repo) => {
|
|
465
|
-
const pin = repositoryPins.get(repo);
|
|
466
|
-
if (!pin) {
|
|
467
|
-
throw new Error(`runtime image source "${repo}" has no repository pin`);
|
|
468
|
-
}
|
|
469
|
-
return pin;
|
|
470
|
-
};
|
|
471
|
-
// A supplemental subject of the runtime-image source index, built at its
|
|
472
|
-
// repository pin, owned by whatever brings it into this edition.
|
|
473
|
-
const addSupplementalSubject = (supplemental, owners) => {
|
|
474
|
-
const repository = requireRepositoryPin(supplemental.repo);
|
|
475
|
-
if (supplemental.sourceCommit !== repository.sourceCommit) {
|
|
476
|
-
throw new Error(
|
|
477
|
-
`runtime subject "${supplemental.kind}:${supplemental.id}" source commit does not match its repository pin`,
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
addSubject(subjects, {
|
|
481
|
-
kind: supplemental.kind,
|
|
482
|
-
id: supplemental.id,
|
|
483
|
-
imageRef: supplemental.imageRef,
|
|
484
|
-
repo: supplemental.repo,
|
|
485
|
-
path: supplemental.path,
|
|
486
|
-
sourceCommit: repository.sourceCommit,
|
|
487
|
-
sourceManifestHash: supplemental.sourceManifestHash,
|
|
488
|
-
sourceManifestPath: supplemental.sourceManifestPath,
|
|
489
|
-
requiredPlatforms,
|
|
490
|
-
owners,
|
|
491
|
-
});
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
for (const biome of lock.biomes) {
|
|
495
|
-
const biomeLabel = `distribution "${lock.distributionId}" biome "${biome.id}"`;
|
|
496
|
-
assertOciBundleSourceOrigin(biome, biomeLabel);
|
|
497
|
-
if (isThirdPartyOciRow(biome)) {
|
|
498
|
-
const pin = thirdPartyPinForRow({ row: biome, pins: thirdPartyPins, declaredSources, label: biomeLabel });
|
|
499
|
-
for (const candidate of thirdPartySubjectCandidates({ row: biome, pin, enums, requiredPlatforms, label: biomeLabel })) {
|
|
500
|
-
addSubject(thirdPartySubjects, candidate);
|
|
501
|
-
}
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
// WHERE THIS BIOME'S IMAGES LIVE, and WHICH COMMIT THEY ARE BUILT FROM.
|
|
505
|
-
// Both answers move together when a biome is drawn from an external source,
|
|
506
|
-
// because both are facts about that source rather than about this fleet.
|
|
507
|
-
//
|
|
508
|
-
// ── THE REGISTRY ──
|
|
509
|
-
//
|
|
510
|
-
// A first-party biome is published to the vendor namespace. A biome drawn
|
|
511
|
-
// from an EXTERNAL source is not, and its coordinate is DECLARED — on the
|
|
512
|
-
// `sources[]` entry as `imageRegistry`, carried into the lock verbatim by
|
|
513
|
-
// `external-source-walk.mjs` and read HERE. This used to refuse
|
|
514
|
-
// unconditionally, including when the coordinate WAS declared, because
|
|
515
|
-
// reading it was not implemented; that was an honest partial and it is now
|
|
516
|
-
// implemented.
|
|
517
|
-
//
|
|
518
|
-
// THE REFUSAL SURVIVES, NARROWED TO THE CASE IT WAS ALWAYS ABOUT: an
|
|
519
|
-
// external source that declares NO registry. The kernel legislates that on
|
|
520
|
-
// the field itself — *"a plane that needs one and finds it absent must
|
|
521
|
-
// REFUSE rather than substitute the first-party registry, which would
|
|
522
|
-
// publish a customer's build into the vendor org."* Absent must never
|
|
523
|
-
// become a fallback to the first-party registry. That substitution is the entire
|
|
524
|
-
// defect; narrowing the refusal must not soften it into a default.
|
|
525
|
-
//
|
|
526
|
-
// ── THE COMMIT ──
|
|
527
|
-
//
|
|
528
|
-
// A first-party biome's commit comes from `runtime-image-source-index.json`
|
|
529
|
-
// via `requireRepositoryPin`. An external biome has no entry there and
|
|
530
|
-
// never will: its `repo` is the source's own LOGICAL name as declared in
|
|
531
|
-
// `sources[]`, not a `repos/<x>` path, so the pin lookup would fail with "has no
|
|
532
|
-
// repository pin" — a true sentence about the wrong plane. Its pin is the
|
|
533
|
-
// commit the WALK resolved and the lock recorded, which is the same fact
|
|
534
|
-
// held by the authority that actually resolved it.
|
|
535
|
-
//
|
|
536
|
-
// ── PLACEMENT ──
|
|
537
|
-
//
|
|
538
|
-
// Both are resolved BEFORE `requireRepositoryPin` and before the
|
|
539
|
-
// `target === 'web'` branch, so services, runtime components and web biomes
|
|
540
|
-
// are covered by one statement rather than leaving the web branch's
|
|
541
|
-
// `continue` as a hole.
|
|
542
|
-
//
|
|
543
|
-
// KNOWN NARROWING, stated rather than discovered: a web biome composes no
|
|
544
|
-
// image ref at all, so an external source shipping ONLY web biomes needs no
|
|
545
|
-
// registry and is nonetheless refused here. That is deliberately left
|
|
546
|
-
// fail-closed — it has no subject today (both externally-sourced biomes in
|
|
547
|
-
// this plane declare `target: "server"`), and loosening a fence for a case
|
|
548
|
-
// that does not exist is how a fence stops meaning anything.
|
|
549
|
-
let imageRegistry = firstPartyImageRegistry();
|
|
550
|
-
// The identity element, not a fallback — see `imageRefForSubject`. A
|
|
551
|
-
// first-party subject is published under its own id, which is exactly what
|
|
552
|
-
// an empty prefix composes.
|
|
553
|
-
let imageNamePrefix = '';
|
|
554
|
-
let sourceCommit;
|
|
555
|
-
if (biome.source === undefined) {
|
|
556
|
-
sourceCommit = requireRepositoryPin(biome.repo).sourceCommit;
|
|
557
|
-
} else {
|
|
558
|
-
if (biome.source.imageRegistry === undefined) {
|
|
559
|
-
throw new Error(
|
|
560
|
-
`distribution "${lock.distributionId}" biome "${biome.id}" was drawn from external ` +
|
|
561
|
-
`source "${biome.source.name}" (${biome.source.url}@${biome.source.commit}), which ` +
|
|
562
|
-
'declares no imageRegistry, so this plane cannot address its images. Composing ' +
|
|
563
|
-
`them under "${firstPartyImageRegistry()}" would address a build this organisation does not ` +
|
|
564
|
-
'produce, inside the vendor namespace. Declare `imageRegistry` on that sources[] ' +
|
|
565
|
-
'entry — refusing rather than substituting the first-party registry.',
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
imageRegistry = biome.source.imageRegistry;
|
|
569
|
-
// THE PREFIX COMES FROM THE DECLARATION, NEVER FROM THE LOCK.
|
|
570
|
-
//
|
|
571
|
-
// This read was `biome.source.imageNamePrefix`, which required the lock to
|
|
572
|
-
// carry the key — and a lock carrying it is refused outright by any
|
|
573
|
-
// consumer on `@xemahq/contracts` `^0.9.x`, whose DistributionLockSchema
|
|
574
|
-
// is `.strict()` and learned the key only in 0.10.0.
|
|
575
|
-
//
|
|
576
|
-
// Removing it from the lock WITHOUT moving this read is what composed
|
|
577
|
-
// `<customer-registry>/pai-core-api` instead of `<customer-registry>/<customer>-pai-core-api`:
|
|
578
|
-
// the `?? ''` swallowed the absence, so an unqualified name in a shared
|
|
579
|
-
// organisation namespace resolved silently. That is worse than a 404 —
|
|
580
|
-
// the first collision overwrites another product's tags.
|
|
581
|
-
//
|
|
582
|
-
// A MISSING DECLARED ENTRY IS THEREFORE REFUSED, never defaulted. An entry
|
|
583
|
-
// that declares NO prefix is a different fact and is honoured: the empty
|
|
584
|
-
// prefix is the identity element, composing a subject under its bare id.
|
|
585
|
-
if (!Array.isArray(declaredSources)) {
|
|
586
|
-
throw new Error(
|
|
587
|
-
`distribution "${lock.distributionId}" biome "${biome.id}" was drawn from ` +
|
|
588
|
-
`external source "${biome.source.name}", but this resolve was given no ` +
|
|
589
|
-
'declared sources[] to read its imageNamePrefix from. That prefix is ' +
|
|
590
|
-
'producer input and is deliberately not carried in the lock; pass ' +
|
|
591
|
-
'`resolveDistributionDeclaredSources(schemas, id)` — refusing rather ' +
|
|
592
|
-
'than composing an unprefixed name.',
|
|
593
|
-
);
|
|
594
|
-
}
|
|
595
|
-
const declaredSource = declaredSources.find(
|
|
596
|
-
(candidate) => candidate.name === biome.source.name,
|
|
597
|
-
);
|
|
598
|
-
if (declaredSource === undefined) {
|
|
599
|
-
throw new Error(
|
|
600
|
-
`distribution "${lock.distributionId}" biome "${biome.id}" was drawn from ` +
|
|
601
|
-
`external source "${biome.source.name}", which this distribution's ` +
|
|
602
|
-
`composed sources[] does not declare (declared: ${
|
|
603
|
-
declaredSources.map((candidate) => candidate.name).join(', ') ||
|
|
604
|
-
'none'
|
|
605
|
-
}). The image name cannot be composed without its declaration — ` +
|
|
606
|
-
'refusing rather than publishing under an unprefixed name in a shared ' +
|
|
607
|
-
"organisation namespace, where the first collision overwrites another " +
|
|
608
|
-
"product's tags.",
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
imageNamePrefix = declaredSource.imageNamePrefix ?? '';
|
|
612
|
-
sourceCommit = biome.source.commit;
|
|
613
|
-
}
|
|
614
|
-
if (biome.target === 'web') {
|
|
615
|
-
webBiomes.push({
|
|
616
|
-
biomeId: biome.id,
|
|
617
|
-
repo: biome.repo,
|
|
618
|
-
path: biome.path,
|
|
619
|
-
sourceCommit,
|
|
620
|
-
sourceManifestHash: biome.sourceManifestHash,
|
|
621
|
-
});
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
// A worker that runs a sibling service's image builds none of its own: the
|
|
625
|
-
// service's subject is the one sealed coordinate, and the deploy plane
|
|
626
|
-
// reaches it through `workerImages[worker].imageOf`. Minting a subject for
|
|
627
|
-
// the worker would name an image nothing builds.
|
|
628
|
-
const sharedImageWorkers = new Set(Object.keys(biome.workerImages ?? {}));
|
|
629
|
-
for (const service of biome.services ?? []) {
|
|
630
|
-
if (sharedImageWorkers.has(service)) {
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
addSubject(subjects, {
|
|
634
|
-
kind: enums.RuntimeImageSubjectKind.Service,
|
|
635
|
-
id: service,
|
|
636
|
-
imageRef: imageRefForSubject(service, imageRegistry, imageNamePrefix),
|
|
637
|
-
repo: biome.repo,
|
|
638
|
-
path: biome.path,
|
|
639
|
-
sourceCommit,
|
|
640
|
-
sourceManifestHash: biome.sourceManifestHash,
|
|
641
|
-
requiredPlatforms,
|
|
642
|
-
owners: [
|
|
643
|
-
{
|
|
644
|
-
kind: enums.RuntimeImageOwnerKind.Biome,
|
|
645
|
-
biomeId: biome.id,
|
|
646
|
-
},
|
|
647
|
-
],
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
// Runtime components are FULL delivery subjects — built, content-addressed,
|
|
651
|
-
// signed, mirrored and air-gapped exactly like a service image. They differ
|
|
652
|
-
// only in that nothing schedules them, which the deploy plane reads off the
|
|
653
|
-
// subject KIND. Their source path resolves through the same
|
|
654
|
-
// `ships.apis[].path` SSOT, because a runtime component IS a `ships.apis[]`
|
|
655
|
-
// entry — it just carries `workload: "runtime-component"`.
|
|
656
|
-
for (const component of biome.runtimeComponents ?? []) {
|
|
657
|
-
addSubject(subjects, {
|
|
658
|
-
kind: enums.RuntimeImageSubjectKind.RuntimeComponent,
|
|
659
|
-
id: component,
|
|
660
|
-
imageRef: imageRefForSubject(component, imageRegistry, imageNamePrefix),
|
|
661
|
-
repo: biome.repo,
|
|
662
|
-
path: biome.path,
|
|
663
|
-
sourceCommit,
|
|
664
|
-
sourceManifestHash: biome.sourceManifestHash,
|
|
665
|
-
requiredPlatforms,
|
|
666
|
-
owners: [
|
|
667
|
-
{
|
|
668
|
-
kind: enums.RuntimeImageOwnerKind.Biome,
|
|
669
|
-
biomeId: biome.id,
|
|
670
|
-
},
|
|
671
|
-
],
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Runtime components a rostered biome DISPATCHES enter the roster as
|
|
677
|
-
// runtime-component rows built from the TARGET biome's index entry, at its
|
|
678
|
-
// repository pin, exactly like a rostered biome's own runtime component. The
|
|
679
|
-
// owner is the rostered biome that dispatches it: the selection that brings
|
|
680
|
-
// the image into this edition. The target biome stays unrostered.
|
|
681
|
-
//
|
|
682
|
-
// A rostered biome's edges are read from ITS index entry, which must describe
|
|
683
|
-
// the very biome the lock pins; one from a declared source is refused at
|
|
684
|
-
// distribution resolve if it dispatches, and the first-party index carries
|
|
685
|
-
// none, so it is not looked up here.
|
|
686
|
-
const dispatchers = lock.biomes
|
|
687
|
-
.filter((biome) => biome.source === undefined && !isThirdPartyOciRow(biome) && biome.target === 'server')
|
|
688
|
-
.map((biome) => {
|
|
689
|
-
const entries = biomeIndex.filter(
|
|
690
|
-
(entry) =>
|
|
691
|
-
entry.id === biome.id &&
|
|
692
|
-
entry.target === biome.target &&
|
|
693
|
-
entry.repo === biome.repo &&
|
|
694
|
-
entry.source === undefined,
|
|
695
|
-
);
|
|
696
|
-
if (entries.length !== 1 || entries[0].sourceManifestHash !== biome.sourceManifestHash) {
|
|
697
|
-
throw new Error(
|
|
698
|
-
`distribution "${lock.distributionId}" biome "${biome.id}" (${biome.repo}, ${biome.sourceManifestHash}) ` +
|
|
699
|
-
`has ${entries.length === 1 ? 'a stale' : `${entries.length}`} biome index ${entries.length === 1 ? 'entry' : 'entries'}, ` +
|
|
700
|
-
'so the runtime components it dispatches cannot be read — re-resolve the edition from the current index',
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
return entries[0];
|
|
704
|
-
});
|
|
705
|
-
for (const dispatched of resolveDispatchedRuntimeComponents(
|
|
706
|
-
`distribution "${lock.distributionId}"`,
|
|
707
|
-
dispatchers,
|
|
708
|
-
biomeIndex,
|
|
709
|
-
)) {
|
|
710
|
-
if (dispatched.supplementalRuntimeComponentId !== undefined) {
|
|
711
|
-
// THE REFUSAL: a dispatched supplemental runtime component must be one the
|
|
712
|
-
// runtime image source index declares as a runtime-component subject.
|
|
713
|
-
// Every image lock is produced here, so this is the one place it is checked.
|
|
714
|
-
const declared = runtimeSourceIndex.supplementalSubjects.find(
|
|
715
|
-
(subject) =>
|
|
716
|
-
subject.kind === enums.RuntimeImageSubjectKind.RuntimeComponent &&
|
|
717
|
-
subject.id === dispatched.supplementalRuntimeComponentId,
|
|
718
|
-
);
|
|
719
|
-
if (declared === undefined) {
|
|
720
|
-
throw new UndeclaredSupplementalRuntimeComponentError(
|
|
721
|
-
dispatched.supplementalRuntimeComponentId,
|
|
722
|
-
dispatched.consumers,
|
|
723
|
-
`distribution "${lock.distributionId}"`,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
addSupplementalSubject(
|
|
727
|
-
declared,
|
|
728
|
-
dispatched.consumers.map((biomeId) => ({ kind: enums.RuntimeImageOwnerKind.Biome, biomeId })),
|
|
729
|
-
);
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
const { target } = dispatched;
|
|
733
|
-
addSubject(subjects, {
|
|
734
|
-
kind: enums.RuntimeImageSubjectKind.RuntimeComponent,
|
|
735
|
-
id: dispatched.image,
|
|
736
|
-
imageRef: imageRefForSubject(dispatched.image, firstPartyImageRegistry(), ''),
|
|
737
|
-
repo: target.repo,
|
|
738
|
-
path: target.path,
|
|
739
|
-
sourceCommit: requireRepositoryPin(target.repo).sourceCommit,
|
|
740
|
-
sourceManifestHash: target.sourceManifestHash,
|
|
741
|
-
requiredPlatforms,
|
|
742
|
-
owners: dispatched.consumers.map((biomeId) => ({
|
|
743
|
-
kind: enums.RuntimeImageOwnerKind.Biome,
|
|
744
|
-
biomeId,
|
|
745
|
-
})),
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
const runtimeBindings = new Map(
|
|
750
|
-
runtimeSourceIndex.platformServiceBindings.map((binding) => [
|
|
751
|
-
binding.platformService,
|
|
752
|
-
binding,
|
|
753
|
-
]),
|
|
754
|
-
);
|
|
755
|
-
const supplementalSubjects = new Map(
|
|
756
|
-
runtimeSourceIndex.supplementalSubjects.map((subject) => [
|
|
757
|
-
subjectKey(subject),
|
|
758
|
-
subject,
|
|
759
|
-
]),
|
|
760
|
-
);
|
|
761
|
-
|
|
762
|
-
// Supplemental subjects a BIOME launches (`launchedBy`): sealed, owned by
|
|
763
|
-
// that biome, in every edition whose roster contains it. The catalogue
|
|
764
|
-
// checks run for every edition, rostered launcher or not.
|
|
765
|
-
const boundSubjects = new Set(
|
|
766
|
-
runtimeSourceIndex.platformServiceBindings.map((binding) => bindingKey(binding)),
|
|
767
|
-
);
|
|
768
|
-
// A biome's `supplemental:<id>` dispatch edge also carries the subject: the
|
|
769
|
-
// catalogue check reads the whole first-party index, not this roster, so a
|
|
770
|
-
// subject reached only by a dispatch is not "unreachable" in any edition.
|
|
771
|
-
const dispatchedSubjects = new Set(
|
|
772
|
-
biomeIndex
|
|
773
|
-
.filter((entry) => entry.source === undefined)
|
|
774
|
-
.flatMap((entry) => entry.dispatchesRuntimeComponents ?? [])
|
|
775
|
-
.filter((edge) => edge.supplementalRuntimeComponentId !== undefined)
|
|
776
|
-
.map((edge) => `${enums.RuntimeImageSubjectKind.RuntimeComponent}\0${edge.supplementalRuntimeComponentId}`),
|
|
777
|
-
);
|
|
778
|
-
const launchers = new Map(
|
|
779
|
-
supplementalSources
|
|
780
|
-
.filter((source) => source.launchedBy !== undefined)
|
|
781
|
-
.map((source) => [subjectKey(source), source.launchedBy]),
|
|
782
|
-
);
|
|
783
|
-
for (const key of launchers.keys()) {
|
|
784
|
-
if (!supplementalSubjects.has(key)) {
|
|
785
|
-
throw new Error(
|
|
786
|
-
`supplemental source "${key.replace('\0', ':')}" declares a launcher, but the runtime image source index ` +
|
|
787
|
-
'has no such supplemental subject — rebuild the index from the current declarations',
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
for (const [key, supplemental] of supplementalSubjects) {
|
|
792
|
-
const launcher = launchers.get(key);
|
|
793
|
-
if (launcher === undefined) {
|
|
794
|
-
if (!boundSubjects.has(key) && !dispatchedSubjects.has(key)) {
|
|
795
|
-
throw new Error(
|
|
796
|
-
`supplemental subject "${supplemental.kind}:${supplemental.id}" has neither a launchedBy, a ` +
|
|
797
|
-
'platform-service binding nor a biome dispatching it, so no edition could ever carry it — declare what launches it ' +
|
|
798
|
-
'(runtimeImageSources.supplementalSources[].launchedBy), bind it to a platform service or dispatch it from a biome',
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
803
|
-
if (!biomeIndex.some((entry) => entry.id === launcher.biomeId && entry.source === undefined)) {
|
|
804
|
-
throw new Error(
|
|
805
|
-
`supplemental subject "${supplemental.kind}:${supplemental.id}" is launched by biome ` +
|
|
806
|
-
`"${launcher.biomeId}", which the first-party biome index does not carry`,
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
if (lock.biomes.some((biome) => biome.id === launcher.biomeId)) {
|
|
810
|
-
addSupplementalSubject(supplemental, [
|
|
811
|
-
{ kind: enums.RuntimeImageOwnerKind.Biome, biomeId: launcher.biomeId },
|
|
812
|
-
]);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
for (const service of lock.platformServices ?? []) {
|
|
817
|
-
let binding;
|
|
818
|
-
// The discriminator is the DECLARED runtime binding, never "does the lock
|
|
819
|
-
// carry a source". Every platform service carries an exact source identity
|
|
820
|
-
// now (`LockedPlatformService.source` is required at lock schemaVersion 2),
|
|
821
|
-
// so `if (service.source)` became unconditionally true — and it would have
|
|
822
|
-
// minted a repository-ROOT `platform` subject for `xema-kernel-server`,
|
|
823
|
-
// `docs-api`, `xema-host-web` and `opencode-image-prepuller`, hashing the
|
|
824
|
-
// wrong closure for three of them and inventing an image that has never
|
|
825
|
-
// existed for the fourth. A binding names the subject that really carries
|
|
826
|
-
// the service; its absence means the service IS its own repository image.
|
|
827
|
-
const runtimeBinding = runtimeBindings.get(service.name);
|
|
828
|
-
if (!runtimeBinding) {
|
|
829
|
-
if (!service.source) {
|
|
830
|
-
throw new Error(
|
|
831
|
-
`platform service "${service.name}" has no declared runtime image binding ` +
|
|
832
|
-
'and no exact source identity, so no image subject can be resolved for it',
|
|
833
|
-
);
|
|
834
|
-
}
|
|
835
|
-
const repository = requireRepositoryPin(service.source.repo);
|
|
836
|
-
if (service.source.sourceCommit !== repository.sourceCommit) {
|
|
837
|
-
throw new Error(
|
|
838
|
-
`platform service "${service.name}" source commit does not match its repository pin`,
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
binding = {
|
|
842
|
-
platformService: service.name,
|
|
843
|
-
subjectKind: enums.RuntimeImageSubjectKind.Platform,
|
|
844
|
-
subjectId: service.source.imageKey,
|
|
845
|
-
};
|
|
846
|
-
addSubject(subjects, {
|
|
847
|
-
kind: binding.subjectKind,
|
|
848
|
-
id: binding.subjectId,
|
|
849
|
-
imageRef: imageRefForSubject(binding.subjectId),
|
|
850
|
-
repo: service.source.repo,
|
|
851
|
-
path: service.source.repo,
|
|
852
|
-
sourceCommit: repository.sourceCommit,
|
|
853
|
-
sourceManifestHash: service.source.sourceManifestHash,
|
|
854
|
-
sourceManifestPath: service.source.path,
|
|
855
|
-
requiredPlatforms,
|
|
856
|
-
owners: [
|
|
857
|
-
{
|
|
858
|
-
kind: enums.RuntimeImageOwnerKind.PlatformService,
|
|
859
|
-
platformService: service.name,
|
|
860
|
-
},
|
|
861
|
-
],
|
|
862
|
-
});
|
|
863
|
-
} else {
|
|
864
|
-
binding = runtimeBinding;
|
|
865
|
-
const key = bindingKey(binding);
|
|
866
|
-
const supplemental = supplementalSubjects.get(key);
|
|
867
|
-
if (supplemental) {
|
|
868
|
-
addSupplementalSubject(supplemental, [
|
|
869
|
-
{
|
|
870
|
-
kind: enums.RuntimeImageOwnerKind.PlatformService,
|
|
871
|
-
platformService: service.name,
|
|
872
|
-
},
|
|
873
|
-
]);
|
|
874
|
-
} else {
|
|
875
|
-
const target = subjects.get(key);
|
|
876
|
-
if (!target) {
|
|
877
|
-
throw new Error(
|
|
878
|
-
missingArtifactDependencyMessage(lock, service.name, binding, biomeIndex),
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
addSubject(subjects, {
|
|
882
|
-
...target,
|
|
883
|
-
owners: [
|
|
884
|
-
{
|
|
885
|
-
kind: enums.RuntimeImageOwnerKind.PlatformService,
|
|
886
|
-
platformService: service.name,
|
|
887
|
-
},
|
|
888
|
-
],
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
platformServiceBindings.push(binding);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// A binding for a service this edition does not ship is outside its artifact
|
|
896
|
-
// graph. It was REFUSED here ("runtime image source index contains unselected
|
|
897
|
-
// platform service"), which made every image-bearing platform service
|
|
898
|
-
// unexcludable: `billing-api` could be dropped only because it happens to be
|
|
899
|
-
// independently sourced and carries no binding, while `docs-api`,
|
|
900
|
-
// `xema-host-web` and `opencode-image-prepuller` could not. The index is the
|
|
901
|
-
// fleet catalogue, not this edition's roster; the roster is derived from the
|
|
902
|
-
// lock's `platformServices`, above. A binding naming a service the COMPOSED
|
|
903
|
-
// catalogue does not know at all is still a declaration defect, and is
|
|
904
|
-
// refused by `build-runtime-image-source-index.mjs --check`, which owns the
|
|
905
|
-
// index — not by every edition that happens to omit the service.
|
|
906
|
-
|
|
907
|
-
const orderedSubjects = [...subjects.values()]
|
|
908
|
-
.map((subject) => ({
|
|
909
|
-
...subject,
|
|
910
|
-
owners: subject.owners.sort(compareOwner),
|
|
911
|
-
}))
|
|
912
|
-
.sort(compareSubject);
|
|
913
|
-
webBiomes.sort((left, right) => compareStrings(left.biomeId, right.biomeId));
|
|
914
|
-
platformServiceBindings.sort((left, right) =>
|
|
915
|
-
compareStrings(left.platformService, right.platformService),
|
|
916
|
-
);
|
|
917
|
-
const orderedThirdPartySubjects = [...thirdPartySubjects.values()]
|
|
918
|
-
.map((subject) => ({ ...subject, owners: subject.owners.sort(compareOwner) }))
|
|
919
|
-
.sort(compareSubject);
|
|
920
|
-
for (const subject of orderedThirdPartySubjects) {
|
|
921
|
-
if (subjects.has(subjectKey(subject))) {
|
|
922
|
-
throw new Error(
|
|
923
|
-
`third-party runtime image subject "${subject.kind}:${subject.id}" has the identity of another subject of distribution "${lock.distributionId}"`,
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
return {
|
|
928
|
-
subjects: orderedSubjects,
|
|
929
|
-
thirdPartySubjects: orderedThirdPartySubjects,
|
|
930
|
-
webBiomes,
|
|
931
|
-
platformServiceBindings,
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
/** Every image subject of a structure, first-party and third-party, in the lock's order. */
|
|
936
|
-
export function structureSubjects(structure) {
|
|
937
|
-
return [...structure.subjects, ...structure.thirdPartySubjects].sort(compareSubject);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* THE COMPOSED-PROCESS IMAGE OF ONE BUILD — where a producing repository
|
|
942
|
-
* declares it, and how an edition addresses it.
|
|
943
|
-
*
|
|
944
|
-
* The kernel (`RuntimeImageSubjectKind.ComposedProcess`) fixes WHAT the subject
|
|
945
|
-
* is: one repository at one commit, carrying every platform-scheduled service
|
|
946
|
-
* of that build and `@xemahq/xema-service-nest`'s `xema-composed-process`
|
|
947
|
-
* entrypoint, owned by the biomes whose services it carries. Neither the kernel
|
|
948
|
-
* nor the SDK fixes WHERE a repository builds it, so this plane does, once:
|
|
949
|
-
*
|
|
950
|
-
* - the image directory is the per-class directory
|
|
951
|
-
* `infra/composed-process/<class>/` at the repository root (`<class>` a
|
|
952
|
-
* kernel `ComposedImageClass`; an edition addresses one composed subject
|
|
953
|
-
* per repository, of the kernel's `DEFAULT_COMPOSED_IMAGE_CLASS`). A commit
|
|
954
|
-
* that predates the per-class layout declares it at the legacy root
|
|
955
|
-
* `infra/composed-process/`, which stays readable because committed release
|
|
956
|
-
* locks still pin such commits. Both at once, or a declaration in another
|
|
957
|
-
* class directory, is refused (`composedProcessDeclarationDir`). The
|
|
958
|
-
* directory holds its own `Dockerfile` and an `image-build-inputs.json` whose
|
|
959
|
-
* `workspacePackages` lists every member service's workspace package
|
|
960
|
-
* directory — the Dockerfile-only image shape `hashImageBuildInputs`
|
|
961
|
-
* already reads (`opencode-worker` is the other one). Its tag is therefore
|
|
962
|
-
* an ordinary `xema-image-input-sha256-v2` key, computed by the one
|
|
963
|
-
* implementation every producer uses, with `SERVICE` = the subject id and
|
|
964
|
-
* `SERVICE_PATH` = this directory;
|
|
965
|
-
* - the DECLARATION is that `image-build-inputs.json` existing in that
|
|
966
|
-
* directory at the pinned commit. A repository that declares none (and has
|
|
967
|
-
* no `infra/composed-process/` tree at all) has no composed image, and an
|
|
968
|
-
* edition carries no composed subject for it — which the deploy plane
|
|
969
|
-
* reports as "the producer builds no composed-process image at that
|
|
970
|
-
* commit". That is an honest absence, not a skipped subject: a DECLARED
|
|
971
|
-
* image that has not been built is still emitted, carries no
|
|
972
|
-
* `imageDigest`, and is undeliverable until it is sealed;
|
|
973
|
-
* - the subject id is `<declared source id>-composed-process`. The source
|
|
974
|
-
* id is DECLARED, never derived from where the repository is hosted: an
|
|
975
|
-
* external source's is the edition's `sources[].name` (which the walk
|
|
976
|
-
* also stamps as the biome's `repo`), a first-party repository's is
|
|
977
|
-
* declared in `first-party-source-ids.mjs`. A rename or host move changes
|
|
978
|
-
* `repositoryUrl` — provenance — and no subject id, `imageRef` or
|
|
979
|
-
* `contentHash` (the id is the `SERVICE` input of the tag). It cannot be
|
|
980
|
-
* a constant: two first-party repositories publish into one registry
|
|
981
|
-
* namespace, and the image lock refuses two subjects sharing an
|
|
982
|
-
* `imageRef`. The producing build publishes under this same name;
|
|
983
|
-
* - `imageRef` is composed exactly as the repository's service subjects are
|
|
984
|
-
* (`imageRefForSubject`, with the same registry and declared prefix).
|
|
985
|
-
*/
|
|
986
|
-
export const COMPOSED_PROCESS_IMAGE_PATH = 'infra/composed-process';
|
|
987
|
-
export const COMPOSED_PROCESS_DECLARATION_FILE = 'image-build-inputs.json';
|
|
988
|
-
const COMPOSED_PROCESS_ID_SUFFIX = '-composed-process';
|
|
989
|
-
|
|
990
|
-
export function composedProcessSubjectId(sourceId) {
|
|
991
|
-
return `${assertDeclaredSourceId(sourceId, 'composed-process subject')}${COMPOSED_PROCESS_ID_SUFFIX}`;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* The DECLARED source id of every repository this edition builds a service
|
|
996
|
-
* image from, keyed by the subject's `repo` handle — the edition's own
|
|
997
|
-
* `sources[].name` for a source-walked biome, the first-party declaration
|
|
998
|
-
* otherwise. Never read off a URL.
|
|
999
|
-
*/
|
|
1000
|
-
export function editionSourceIds(lock) {
|
|
1001
|
-
const ids = new Map();
|
|
1002
|
-
for (const biome of lock.biomes ?? []) {
|
|
1003
|
-
if (biome.source === undefined) {continue;}
|
|
1004
|
-
if (biome.repo !== biome.source.name) {
|
|
1005
|
-
throw new Error(
|
|
1006
|
-
`lock biome "${biome.id}" is drawn from source "${biome.source.name}" but its repo ` +
|
|
1007
|
-
`handle is "${biome.repo}" — a source-walked biome's repo IS its declared source id`,
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
ids.set(biome.repo, assertDeclaredSourceId(biome.source.name, `lock biome "${biome.id}" source`));
|
|
1011
|
-
}
|
|
1012
|
-
return { get: (repo) => ids.get(repo) ?? firstPartySourceId(repo) };
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* A composed-process declaration the build cannot be read from. Named so a
|
|
1017
|
-
* caller can tell "this image's inputs are unknown" from any other refusal:
|
|
1018
|
-
* the carried set is evidence, and with none there is no owner to derive.
|
|
1019
|
-
*/
|
|
1020
|
-
export class ComposedProcessWorkerError extends Error {
|
|
1021
|
-
constructor(message) {
|
|
1022
|
-
super(message);
|
|
1023
|
-
this.name = 'ComposedProcessWorkerError';
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
export class ComposedProcessInputsError extends Error {
|
|
1028
|
-
constructor(message) {
|
|
1029
|
-
super(message);
|
|
1030
|
-
this.name = 'ComposedProcessInputsError';
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
/**
|
|
1035
|
-
* Which of a repository's biome services the composed image CARRIES, read from
|
|
1036
|
-
* the composed build's own inputs at the sealed commit.
|
|
1037
|
-
*
|
|
1038
|
-
* `image-build-inputs.json` lists the member package directories the image is
|
|
1039
|
-
* built from (its hash is the subject's `sourceManifestHash`). A service is
|
|
1040
|
-
* carried when its manifest-declared source directory (`subjectServicePath`,
|
|
1041
|
-
* the `components[].artifact.path` mapping) is one of them. Returns one row
|
|
1042
|
-
* per biome: the service ids carried and the ones not.
|
|
1043
|
-
*/
|
|
1044
|
-
/** A component's declared `imageClass` validated with the kernel's own schema; absent stays undefined (the default applies). */
|
|
1045
|
-
function declaredImageClass(declared, subject, enums) {
|
|
1046
|
-
if (declared === undefined) {return undefined;}
|
|
1047
|
-
const parsed = enums.ComposedImageClassSchema.safeParse(declared);
|
|
1048
|
-
if (!parsed.success) {
|
|
1049
|
-
throw new ComposedProcessInputsError(
|
|
1050
|
-
`service "${subject.id}" of ${subject.repo} declares components[].imageClass ${JSON.stringify(declared)}, which is not an image class the kernel knows`,
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
|
-
return parsed.data;
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function composedProcessCarriage(structure, enums, repo, sourceCommit, locate, declarationDir) {
|
|
1057
|
-
const inputsPath = `${declarationDir}/${COMPOSED_PROCESS_DECLARATION_FILE}`;
|
|
1058
|
-
const { dir } = locate(repo);
|
|
1059
|
-
let members;
|
|
1060
|
-
try {
|
|
1061
|
-
members = readGitJsonAtCommit(dir, inputsPath, sourceCommit)?.workspacePackages;
|
|
1062
|
-
} catch (cause) {
|
|
1063
|
-
throw new ComposedProcessInputsError(
|
|
1064
|
-
`composed-process inputs "${inputsPath}" of "${repo}" are unreadable at ${sourceCommit} ` +
|
|
1065
|
-
`(${cause.message}) — the biomes the image carries cannot be derived`,
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
if (!Array.isArray(members) || members.some((member) => typeof member !== 'string')) {
|
|
1069
|
-
throw new ComposedProcessInputsError(
|
|
1070
|
-
`composed-process inputs "${inputsPath}" of "${repo}" at ${sourceCommit} declare no ` +
|
|
1071
|
-
'"workspacePackages" array of directories — the biomes the image carries cannot be derived',
|
|
1072
|
-
);
|
|
1073
|
-
}
|
|
1074
|
-
const carried = new Set(members);
|
|
1075
|
-
const byBiome = new Map();
|
|
1076
|
-
for (const subject of structure.subjects) {
|
|
1077
|
-
if (subject.kind !== enums.RuntimeImageSubjectKind.Service || subject.repo !== repo) {continue;}
|
|
1078
|
-
const { path: servicePath, componentKind, imageClass: declaredClass } = subjectServiceSource(subject, sourceCommit, locate);
|
|
1079
|
-
const imageClass = declaredImageClass(declaredClass, subject, enums);
|
|
1080
|
-
for (const owner of subject.owners) {
|
|
1081
|
-
if (owner.kind !== enums.RuntimeImageOwnerKind.Biome) {continue;}
|
|
1082
|
-
const row = byBiome.get(owner.biomeId) ??
|
|
1083
|
-
{ biomeId: owner.biomeId, carried: [], uncarried: [], carriedWorkers: [], packageDirs: {}, imageClasses: {} };
|
|
1084
|
-
if (!carried.has(servicePath)) {
|
|
1085
|
-
row.uncarried.push(subject.id);
|
|
1086
|
-
} else if (componentKind === 'worker') {
|
|
1087
|
-
// A worker is never a member: it runs as its own Deployment.
|
|
1088
|
-
row.carriedWorkers.push(subject.id);
|
|
1089
|
-
} else {
|
|
1090
|
-
row.carried.push(subject.id);
|
|
1091
|
-
// The member's package directory IS the composed build's own input entry.
|
|
1092
|
-
row.packageDirs[subject.id] = servicePath;
|
|
1093
|
-
// The class its manifest declares (`components[].imageClass`); absent stays undefined (the kernel default applies).
|
|
1094
|
-
row.imageClasses[subject.id] = imageClass;
|
|
1095
|
-
}
|
|
1096
|
-
byBiome.set(owner.biomeId, row);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
return [...byBiome.values()]
|
|
1100
|
-
.map((row) => ({
|
|
1101
|
-
...row,
|
|
1102
|
-
carried: row.carried.sort(compareStrings),
|
|
1103
|
-
uncarried: row.uncarried.sort(compareStrings),
|
|
1104
|
-
carriedWorkers: row.carriedWorkers.sort(compareStrings),
|
|
1105
|
-
}))
|
|
1106
|
-
.sort((a, b) => compareStrings(a.biomeId, b.biomeId));
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function gitPathExistsAtCommit(dir, sourceCommit, repoPath) {
|
|
1110
|
-
try {
|
|
1111
|
-
execFileSync('git', ['-C', dir, 'cat-file', '-e', `${sourceCommit}:${repoPath}`], {
|
|
1112
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1113
|
-
});
|
|
1114
|
-
return true;
|
|
1115
|
-
} catch {
|
|
1116
|
-
return false;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
/**
|
|
1121
|
-
* The repo-relative directory holding a repository's composed-process
|
|
1122
|
-
* DECLARATION at one commit, or null when it declares none. The default class's
|
|
1123
|
-
* directory and the legacy root are the two places an edition reads; a
|
|
1124
|
-
* declaration it cannot address (both present, another class, an
|
|
1125
|
-
* `infra/composed-process/` tree without a recognised one) is refused, because
|
|
1126
|
-
* a declaration the resolver cannot find must never read as "declares nothing".
|
|
1127
|
-
*/
|
|
1128
|
-
function composedProcessDeclarationDir(dir, sourceCommit, enums) {
|
|
1129
|
-
const defaultClass = enums.DEFAULT_COMPOSED_IMAGE_CLASS;
|
|
1130
|
-
const classDir = `${COMPOSED_PROCESS_IMAGE_PATH}/${defaultClass}`;
|
|
1131
|
-
const declares = (candidate) =>
|
|
1132
|
-
gitPathExistsAtCommit(dir, sourceCommit, `${candidate}/${COMPOSED_PROCESS_DECLARATION_FILE}`);
|
|
1133
|
-
const inClassDir = declares(classDir);
|
|
1134
|
-
const atLegacyRoot = declares(COMPOSED_PROCESS_IMAGE_PATH);
|
|
1135
|
-
if (inClassDir && atLegacyRoot) {
|
|
1136
|
-
throw new ComposedProcessInputsError(
|
|
1137
|
-
`ambiguous composed-process declaration at ${sourceCommit}: both "${classDir}/${COMPOSED_PROCESS_DECLARATION_FILE}" ` +
|
|
1138
|
-
`and the legacy "${COMPOSED_PROCESS_IMAGE_PATH}/${COMPOSED_PROCESS_DECLARATION_FILE}" exist`,
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
const otherClasses = enums.ComposedImageClassSchema.options
|
|
1142
|
-
.filter((candidate) => candidate !== defaultClass)
|
|
1143
|
-
.filter((candidate) => declares(`${COMPOSED_PROCESS_IMAGE_PATH}/${candidate}`));
|
|
1144
|
-
if (otherClasses.length > 0) {
|
|
1145
|
-
throw new ComposedProcessInputsError(
|
|
1146
|
-
`composed-process declaration at ${sourceCommit} is in the "${otherClasses.join('", "')}" class ` +
|
|
1147
|
-
`director${otherClasses.length > 1 ? 'ies' : 'y'}, but an edition addresses one composed-process subject per ` +
|
|
1148
|
-
`repository, of the default class "${defaultClass}"; a "${otherClasses[0]}" image has no subject naming yet`,
|
|
1149
|
-
);
|
|
1150
|
-
}
|
|
1151
|
-
if (inClassDir) {return classDir;}
|
|
1152
|
-
if (atLegacyRoot) {return COMPOSED_PROCESS_IMAGE_PATH;}
|
|
1153
|
-
if (gitPathExistsAtCommit(dir, sourceCommit, COMPOSED_PROCESS_IMAGE_PATH)) {
|
|
1154
|
-
throw new ComposedProcessInputsError(
|
|
1155
|
-
`"${COMPOSED_PROCESS_IMAGE_PATH}" exists at ${sourceCommit} but declares no composed-process image the edition can ` +
|
|
1156
|
-
`read: looked at "${classDir}/${COMPOSED_PROCESS_DECLARATION_FILE}" and ` +
|
|
1157
|
-
`"${COMPOSED_PROCESS_IMAGE_PATH}/${COMPOSED_PROCESS_DECLARATION_FILE}"`,
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1160
|
-
return null;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* Read each repository's composed-process DECLARATION at the commit the
|
|
1165
|
-
* edition builds its services from. Only repositories that contribute a
|
|
1166
|
-
* service subject are asked: a composed image carries services, so a
|
|
1167
|
-
* repository shipping none has nothing to compose.
|
|
1168
|
-
*/
|
|
1169
|
-
export function readComposedProcessDeclarations(
|
|
1170
|
-
structure,
|
|
1171
|
-
enums,
|
|
1172
|
-
sourceIds,
|
|
1173
|
-
locate = FIRST_PARTY_TREES,
|
|
1174
|
-
) {
|
|
1175
|
-
const commits = new Map();
|
|
1176
|
-
for (const subject of structure.subjects) {
|
|
1177
|
-
if (subject.kind !== enums.RuntimeImageSubjectKind.Service) {continue;}
|
|
1178
|
-
if (!subject.owners.some((owner) => owner.kind === enums.RuntimeImageOwnerKind.Biome)) {continue;}
|
|
1179
|
-
commits.set(subject.repo, subject.sourceCommit);
|
|
1180
|
-
}
|
|
1181
|
-
const declarations = [];
|
|
1182
|
-
for (const [repo, sourceCommit] of [...commits].sort(([a], [b]) => compareStrings(a, b))) {
|
|
1183
|
-
const { dir, base } = locate(repo);
|
|
1184
|
-
// ABSENCE IS ONLY AN ANSWER ABOUT A COMMIT THAT WAS READ. A repository
|
|
1185
|
-
// missing from this layout, or one lacking the pinned commit, would
|
|
1186
|
-
// otherwise read as "declares nothing", and `dist-check` would grade a lock
|
|
1187
|
-
// that dropped the composed subject as up to date.
|
|
1188
|
-
try {
|
|
1189
|
-
execFileSync('git', ['-C', dir, 'cat-file', '-e', `${sourceCommit}^{commit}`], {
|
|
1190
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1191
|
-
});
|
|
1192
|
-
} catch {
|
|
1193
|
-
throw new Error(
|
|
1194
|
-
`runtime image source "${repo}" pins commit "${sourceCommit}", which is absent from ` +
|
|
1195
|
-
`${dir} — its composed-process declaration cannot be read; fetch the repository ` +
|
|
1196
|
-
'before resolving',
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
const declarationDir = composedProcessDeclarationDir(dir, sourceCommit, enums);
|
|
1200
|
-
if (declarationDir === null) {continue;} // declares no composed-process image at this commit
|
|
1201
|
-
const declarationPath = `${declarationDir}/${COMPOSED_PROCESS_DECLARATION_FILE}`;
|
|
1202
|
-
const sourceId = sourceIds.get(repo);
|
|
1203
|
-
const repoRelative = relative(base, dir).split(sep).join('/');
|
|
1204
|
-
declarations.push({
|
|
1205
|
-
repo,
|
|
1206
|
-
sourceId,
|
|
1207
|
-
sourceCommit,
|
|
1208
|
-
carriage: composedProcessCarriage(structure, enums, repo, sourceCommit, locate, declarationDir),
|
|
1209
|
-
path: `${repoRelative}/${declarationDir}`,
|
|
1210
|
-
sourceManifestPath: `${repoRelative}/${declarationPath}`,
|
|
1211
|
-
sourceManifestHash: exactSourceManifestHash(dir, declarationPath, sourceCommit),
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
return declarations;
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
/**
|
|
1218
|
-
* The image class of one composed artifact, DERIVED from its members' manifests: each member's `components[].imageClass`,
|
|
1219
|
-
* absent meaning the kernel's `DEFAULT_COMPOSED_IMAGE_CLASS`. All members must agree; a mixed artifact is refused naming
|
|
1220
|
-
* the artifact and the members of each class.
|
|
1221
|
-
*/
|
|
1222
|
-
export function composedArtifactImageClass(artifactId, memberRows, declaredClassOf, enums) {
|
|
1223
|
-
const byClass = new Map();
|
|
1224
|
-
for (const { service } of memberRows) {
|
|
1225
|
-
const declared = declaredClassOf.get(service);
|
|
1226
|
-
const imageClass = declared ?? enums.DEFAULT_COMPOSED_IMAGE_CLASS;
|
|
1227
|
-
byClass.set(imageClass, [...(byClass.get(imageClass) ?? []), service]);
|
|
1228
|
-
}
|
|
1229
|
-
if (byClass.size > 1) {
|
|
1230
|
-
const detail = [...byClass].sort(([a], [b]) => compareStrings(a, b)).map(([imageClass, services]) => `${imageClass}: ${services.join(', ')}`).join('; ');
|
|
1231
|
-
throw new ComposedProcessInputsError(
|
|
1232
|
-
`composed-process artifact "${artifactId}" mixes image classes across its members (${detail}) — ` +
|
|
1233
|
-
'an artifact has one class; split the members or align their components[].imageClass',
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
return [...byClass.keys()][0];
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
/** The services the edition SELECTS, read off its distribution lock: every service of every non-web biome whose image is its own (a shared-image worker is not one). */
|
|
1240
|
-
export function editionRosterServices(distributionLock) {
|
|
1241
|
-
return distributionLock.biomes
|
|
1242
|
-
.filter((biome) => biome.target !== 'web')
|
|
1243
|
-
.flatMap((biome) => {
|
|
1244
|
-
const shared = new Set(Object.keys(biome.workerImages ?? {}));
|
|
1245
|
-
return (biome.services ?? []).filter((service) => !shared.has(service));
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
/**
|
|
1250
|
-
* Refuse a build that does not cover every service of the edition roster: each must be a dedicated service subject or a
|
|
1251
|
-
* member of a built composed artifact. A service neither is never dropped from the lock silently, and the carry-forward
|
|
1252
|
-
* (which sees only what was built) is never the one to lose it.
|
|
1253
|
-
*/
|
|
1254
|
-
export function assertBuiltCoversEditionRoster({ rosterServices, serviceSubjects, composedBuilt }) {
|
|
1255
|
-
const covered = new Set([
|
|
1256
|
-
...serviceSubjects.map((subject) => subject.id),
|
|
1257
|
-
...composedBuilt.flatMap((artifact) => (artifact.members ?? []).map((member) => member.service)),
|
|
1258
|
-
]);
|
|
1259
|
-
const missing = [...new Set(rosterServices)].filter((service) => !covered.has(service)).sort(compareStrings);
|
|
1260
|
-
if (missing.length > 0) {
|
|
1261
|
-
throw new ComposedProcessInputsError(
|
|
1262
|
-
`the build covers no image for ${missing.map((service) => `"${service}"`).join(', ')} of the edition roster — ` +
|
|
1263
|
-
'a service of the edition is never dropped from the lock',
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
/** The members of a composed subject with the key of each: the member service's own image-input hash (the one function the packager's tag is emitted from). */
|
|
1269
|
-
export function composedMemberKeys({ subject, repoDirectory, sourceCommit, crossRepoInputsByConsumer, buildStepDirectories, hash = exactImageBuildInputHash }) {
|
|
1270
|
-
return subject.members.map((member) => ({
|
|
1271
|
-
...member,
|
|
1272
|
-
key: hash(
|
|
1273
|
-
{
|
|
1274
|
-
repoDir: repoDirectory,
|
|
1275
|
-
service: member.service,
|
|
1276
|
-
servicePath: member.packageDir,
|
|
1277
|
-
commit: sourceCommit,
|
|
1278
|
-
crossRepoInputs: [
|
|
1279
|
-
...(crossRepoInputsByConsumer.get(`${subject.repo}\u0000`) ?? []),
|
|
1280
|
-
...(crossRepoInputsByConsumer.get(`${subject.repo}\u0000${member.service}`) ?? []),
|
|
1281
|
-
],
|
|
1282
|
-
buildStepDirectories,
|
|
1283
|
-
},
|
|
1284
|
-
imageInputSourceHashAlgorithm(),
|
|
1285
|
-
),
|
|
1286
|
-
}));
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
/**
|
|
1290
|
-
* Add ONE `composed-process` subject per declaring repository to an edition's
|
|
1291
|
-
* structure. Pure over its inputs, so the structural check and the source
|
|
1292
|
-
* resolve derive the same subject from the same declarations.
|
|
1293
|
-
*
|
|
1294
|
-
* Owners are the biomes the image CARRIES, derived from the composed build's
|
|
1295
|
-
* inputs at the sealed commit (`declaration.carriage`): every biome at least one
|
|
1296
|
-
* of whose services is a member, never every biome with a service in the
|
|
1297
|
-
* repository. The members leave the structure and are sealed on the subject
|
|
1298
|
-
* as `members[{ service, packageDir }]` — the per-service membership truth: a
|
|
1299
|
-
* service runs from the composed digest if and only if it is a row (owners
|
|
1300
|
-
* never resolve a service). The edition carries the composed subject plus the
|
|
1301
|
-
* services that are no row (the specials, which keep their own subjects even
|
|
1302
|
-
* inside an owner biome), and nothing addresses a member's own image. A worker
|
|
1303
|
-
* is never a row; an own-image worker the composed build carries is refused.
|
|
1304
|
-
*/
|
|
1305
|
-
export function withComposedProcessSubjects(
|
|
1306
|
-
structure,
|
|
1307
|
-
declarations,
|
|
1308
|
-
enums,
|
|
1309
|
-
requiredPlatforms,
|
|
1310
|
-
{ onNotice = (message) => console.warn(`[dist:images] ${message}`) } = {},
|
|
1311
|
-
) {
|
|
1312
|
-
if (declarations.length === 0) {return structure;}
|
|
1313
|
-
const subjects = new Map(structure.subjects.map((subject) => [subjectKey(subject), subject]));
|
|
1314
|
-
// A biome's members run from ONE composed image: a biome owning two composed
|
|
1315
|
-
// subjects would leave its pods two images to choose between.
|
|
1316
|
-
const composedOwnerOf = new Map();
|
|
1317
|
-
for (const declaration of declarations) {
|
|
1318
|
-
const services = structure.subjects.filter(
|
|
1319
|
-
(subject) =>
|
|
1320
|
-
subject.kind === enums.RuntimeImageSubjectKind.Service && subject.repo === declaration.repo,
|
|
1321
|
-
);
|
|
1322
|
-
if (!Array.isArray(declaration.carriage)) {
|
|
1323
|
-
throw new ComposedProcessInputsError(
|
|
1324
|
-
`composed-process declaration for "${declaration.repo}" carries no evidence of the ` +
|
|
1325
|
-
'biomes the image carries — refusing to guess its owners',
|
|
1326
|
-
);
|
|
1327
|
-
}
|
|
1328
|
-
// Owners are the biomes ANY of whose services the image carries. A biome
|
|
1329
|
-
// with only some carried is an owner too; its services the image does not
|
|
1330
|
-
// carry are specials and keep their own subjects (named here). A biome with
|
|
1331
|
-
// none carried is simply not one.
|
|
1332
|
-
const biomeOwners = [];
|
|
1333
|
-
const members = new Set();
|
|
1334
|
-
const packageDirOf = new Map();
|
|
1335
|
-
const imageClassOf = new Map();
|
|
1336
|
-
for (const row of declaration.carriage) {
|
|
1337
|
-
// A worker is never a member. One the composed build carries must run
|
|
1338
|
-
// from a carried sibling's image (`workerImages`, declared in the
|
|
1339
|
-
// manifest), never be dropped into the composed process or fall back.
|
|
1340
|
-
for (const worker of row.carriedWorkers ?? []) {
|
|
1341
|
-
throw new ComposedProcessWorkerError(
|
|
1342
|
-
row.carried.length === 0
|
|
1343
|
-
? `composed-process "${declaration.repo}" carries worker "${worker}" of biome "${row.biomeId}", ` +
|
|
1344
|
-
'and that biome has no carried service whose image it could run — a worker is never a ' +
|
|
1345
|
-
'composed member; carry a service of its biome or take the worker out of the composed inputs'
|
|
1346
|
-
: `composed-process "${declaration.repo}" carries worker "${worker}" of biome "${row.biomeId}" as ` +
|
|
1347
|
-
'its own oci-image — a worker is never a composed member; declare it `artifact.kind: ' +
|
|
1348
|
-
`shared-oci-image\` of a carried service (${row.carried.join(', ')}) or take it out of the composed inputs`,
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
if (row.carried.length === 0) {continue;}
|
|
1352
|
-
if (row.uncarried.length > 0) {
|
|
1353
|
-
onNotice(
|
|
1354
|
-
`composed-process "${declaration.repo}": biome "${row.biomeId}" is partly carried ` +
|
|
1355
|
-
`(carried: ${row.carried.join(', ')}; not carried: ${row.uncarried.join(', ')}) — ` +
|
|
1356
|
-
'its uncarried services keep their own subjects',
|
|
1357
|
-
);
|
|
1358
|
-
}
|
|
1359
|
-
const other = composedOwnerOf.get(row.biomeId);
|
|
1360
|
-
if (other !== undefined) {
|
|
1361
|
-
throw new Error(
|
|
1362
|
-
`biome "${row.biomeId}" is carried by the composed-process images of both "${other}" and ` +
|
|
1363
|
-
`"${declaration.repo}" — a biome is owned by exactly one composed subject`,
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
composedOwnerOf.set(row.biomeId, declaration.repo);
|
|
1367
|
-
biomeOwners.push({ kind: enums.RuntimeImageOwnerKind.Biome, biomeId: row.biomeId });
|
|
1368
|
-
for (const id of row.carried) {members.add(id);}
|
|
1369
|
-
for (const [id, packageDir] of Object.entries(row.packageDirs ?? {})) {packageDirOf.set(id, packageDir);}
|
|
1370
|
-
for (const [id, imageClass] of Object.entries(row.imageClasses ?? {})) {imageClassOf.set(id, imageClass);}
|
|
1371
|
-
}
|
|
1372
|
-
if (biomeOwners.length === 0) {
|
|
1373
|
-
throw new Error(
|
|
1374
|
-
`composed-process declaration for "${declaration.repo}" carries no biome's services in ` +
|
|
1375
|
-
'this edition — a composed image carries services, so it has no owner',
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
for (const subject of services) {
|
|
1379
|
-
if (subject.sourceCommit !== declaration.sourceCommit) {
|
|
1380
|
-
throw new Error(
|
|
1381
|
-
`runtime subject "${subject.kind}:${subject.id}" is resolved at ${subject.sourceCommit}, ` +
|
|
1382
|
-
`but "${declaration.repo}"'s composed-process image is declared at ` +
|
|
1383
|
-
`${declaration.sourceCommit} — one composed image is ONE build`,
|
|
1384
|
-
);
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
// The registry and declared prefix the repository's services are published
|
|
1388
|
-
// under: `imageRefForSubject` is `<registry>/<prefix><id>`, so stripping
|
|
1389
|
-
// the service id recovers it exactly. Two services disagreeing would mean
|
|
1390
|
-
// one build published under two coordinates, which nothing can address.
|
|
1391
|
-
const namespaces = new Set(
|
|
1392
|
-
services.map((subject) => {
|
|
1393
|
-
if (!subject.imageRef.endsWith(subject.id)) {
|
|
1394
|
-
throw new Error(
|
|
1395
|
-
`runtime subject "${subject.kind}:${subject.id}" imageRef "${subject.imageRef}" ` +
|
|
1396
|
-
'does not end with its id, so its namespace cannot be recovered',
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1399
|
-
return subject.imageRef.slice(0, -subject.id.length);
|
|
1400
|
-
}),
|
|
1401
|
-
);
|
|
1402
|
-
if (namespaces.size !== 1) {
|
|
1403
|
-
throw new Error(
|
|
1404
|
-
`"${declaration.repo}"'s services publish under ${namespaces.size} namespaces ` +
|
|
1405
|
-
`(${[...namespaces].join(', ')}) — a composed image of one build has one`,
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
// A MEMBER reaches its pods through this one subject (every member runs as a
|
|
1409
|
-
// group-of-one of the composed image), so it carries no subject of its own:
|
|
1410
|
-
// no `<imageRef>:<contentHash>` of its own is probed, verified or sealed. A
|
|
1411
|
-
// member is a service of this build the composed build's inputs carry
|
|
1412
|
-
// (`declaration.carriage`), every owner of which is a biome owning the
|
|
1413
|
-
// composed image. Any other service (not carried, or owned by anything but
|
|
1414
|
-
// a biome, e.g. shared with a platform service) keeps its own.
|
|
1415
|
-
const composedOwners = new Set(biomeOwners.map((owner) => owner.biomeId));
|
|
1416
|
-
const memberRows = [];
|
|
1417
|
-
for (const subject of services) {
|
|
1418
|
-
if (
|
|
1419
|
-
members.has(subject.id) &&
|
|
1420
|
-
subject.owners.length > 0 &&
|
|
1421
|
-
subject.owners.every(
|
|
1422
|
-
(owner) => owner.kind === enums.RuntimeImageOwnerKind.Biome && composedOwners.has(owner.biomeId),
|
|
1423
|
-
)
|
|
1424
|
-
) {
|
|
1425
|
-
subjects.delete(subjectKey(subject));
|
|
1426
|
-
const packageDir = packageDirOf.get(subject.id);
|
|
1427
|
-
if (packageDir === undefined) {
|
|
1428
|
-
throw new ComposedProcessInputsError(
|
|
1429
|
-
`composed-process "${declaration.repo}": member "${subject.id}" has no package directory in ` +
|
|
1430
|
-
'the composed build inputs — its shared-image workers would have nowhere to run from',
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
memberRows.push({ service: subject.id, packageDir });
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
if (memberRows.length === 0) {
|
|
1437
|
-
throw new ComposedProcessInputsError(
|
|
1438
|
-
`composed-process "${declaration.repo}" has no member: every service its build carries ` +
|
|
1439
|
-
`(${[...members].sort(compareStrings).join(', ')}) is also owned by something other than a biome ` +
|
|
1440
|
-
'it owns (e.g. a platform service), so each keeps its own subject — a composed image with no ' +
|
|
1441
|
-
'member runs nothing; take it out of the edition or carry a biome-owned service',
|
|
1442
|
-
);
|
|
1443
|
-
}
|
|
1444
|
-
memberRows.sort((a, b) => compareStrings(a.service, b.service));
|
|
1445
|
-
const id = composedProcessSubjectId(declaration.sourceId);
|
|
1446
|
-
const imageClass = composedArtifactImageClass(id, memberRows, imageClassOf, enums);
|
|
1447
|
-
addSubject(subjects, {
|
|
1448
|
-
kind: enums.RuntimeImageSubjectKind.ComposedProcess,
|
|
1449
|
-
id,
|
|
1450
|
-
imageRef: `${[...namespaces][0]}${id}`,
|
|
1451
|
-
repo: declaration.repo,
|
|
1452
|
-
path: declaration.path,
|
|
1453
|
-
sourceCommit: declaration.sourceCommit,
|
|
1454
|
-
sourceManifestHash: declaration.sourceManifestHash,
|
|
1455
|
-
sourceManifestPath: declaration.sourceManifestPath,
|
|
1456
|
-
requiredPlatforms,
|
|
1457
|
-
owners: biomeOwners,
|
|
1458
|
-
members: memberRows,
|
|
1459
|
-
imageClass,
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
return {
|
|
1463
|
-
...structure,
|
|
1464
|
-
subjects: [...subjects.values()]
|
|
1465
|
-
.map((subject) => ({ ...subject, owners: [...subject.owners].sort(compareOwner) }))
|
|
1466
|
-
.sort(compareSubject),
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
/**
|
|
1471
|
-
* The edition structure INCLUDING its composed-process subjects — the one
|
|
1472
|
-
* entry point the source resolve and the structural check share, so neither
|
|
1473
|
-
* can carry a subject the other does not. A repository that declares no
|
|
1474
|
-
* composed image at its pinned commit contributes no subject.
|
|
1475
|
-
*/
|
|
1476
|
-
export function resolveComposedProcessStructure(
|
|
1477
|
-
lock,
|
|
1478
|
-
structure,
|
|
1479
|
-
enums,
|
|
1480
|
-
requiredPlatforms,
|
|
1481
|
-
{ locate, onNotice } = {},
|
|
1482
|
-
) {
|
|
1483
|
-
return withComposedProcessSubjects(
|
|
1484
|
-
structure,
|
|
1485
|
-
readComposedProcessDeclarations(
|
|
1486
|
-
structure,
|
|
1487
|
-
enums,
|
|
1488
|
-
editionSourceIds(lock),
|
|
1489
|
-
// Materialised only when something will be read through it.
|
|
1490
|
-
locate ?? sourceTreeLocator(externalSourceTrees(lock)),
|
|
1491
|
-
),
|
|
1492
|
-
enums,
|
|
1493
|
-
requiredPlatforms,
|
|
1494
|
-
onNotice === undefined ? undefined : { onNotice },
|
|
1495
|
-
);
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
/**
|
|
1499
|
-
* The exact commit every image source repository is resolved at.
|
|
1500
|
-
*
|
|
1501
|
-
* The PIN is the authority, not the local checkout. Requiring each submodule's
|
|
1502
|
-
* `HEAD` to equal its pin made regeneration impossible in any working tree that
|
|
1503
|
-
* had moved on — which is every working tree — and bought nothing: the hashes
|
|
1504
|
-
* read immutable Git objects at the pinned commit, so what is checked out
|
|
1505
|
-
* cannot change a digest. What DOES have to hold is that the pinned commit is
|
|
1506
|
-
* present in the local object database, which is asserted here.
|
|
1507
|
-
*/
|
|
1508
|
-
function sourceCommitMap(structure, locate = FIRST_PARTY_TREES) {
|
|
1509
|
-
const commitByRepo = new Map();
|
|
1510
|
-
for (const source of [...structure.subjects, ...structure.webBiomes]) {
|
|
1511
|
-
if (!source.sourceCommit) {continue;}
|
|
1512
|
-
const incumbent = commitByRepo.get(source.repo);
|
|
1513
|
-
if (incumbent && incumbent !== source.sourceCommit) {
|
|
1514
|
-
throw new Error(
|
|
1515
|
-
`runtime image roster pins multiple commits for "${source.repo}"`,
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1518
|
-
commitByRepo.set(source.repo, source.sourceCommit);
|
|
1519
|
-
}
|
|
1520
|
-
const repositories = [
|
|
1521
|
-
...new Set([
|
|
1522
|
-
...structure.subjects.map((subject) => subject.repo),
|
|
1523
|
-
...structure.webBiomes.map((biome) => biome.repo),
|
|
1524
|
-
]),
|
|
1525
|
-
].sort(compareStrings);
|
|
1526
|
-
for (const repo of repositories) {
|
|
1527
|
-
const commit = commitByRepo.get(repo);
|
|
1528
|
-
if (!commit) {
|
|
1529
|
-
throw new Error(`runtime image source "${repo}" has no pinned commit`);
|
|
1530
|
-
}
|
|
1531
|
-
try {
|
|
1532
|
-
execFileSync(
|
|
1533
|
-
'git',
|
|
1534
|
-
['-C', locate(repo).dir, 'cat-file', '-e', `${commit}^{commit}`],
|
|
1535
|
-
{ stdio: ['ignore', 'ignore', 'pipe'] },
|
|
1536
|
-
);
|
|
1537
|
-
} catch {
|
|
1538
|
-
throw new Error(
|
|
1539
|
-
`runtime image source "${repo}" pins commit "${commit}", which is absent ` +
|
|
1540
|
-
`from the local object database — fetch the repository before resolving`,
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return commitByRepo;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
/**
|
|
1548
|
-
* The SERVICE directory an image is built from, repository-relative.
|
|
1549
|
-
*
|
|
1550
|
-
* A biome subject's `path` is the BIOME directory, and a biome can ship several
|
|
1551
|
-
* services — `agent-runtime` ships four. Hashing the biome directory would give
|
|
1552
|
-
* all four the same closure and would move all four when any one of them
|
|
1553
|
-
* changes, so the service directory is read from the manifest SSOT
|
|
1554
|
-
* (`xema.ships.apis[].path`) rather than synthesized. Non-biome subjects
|
|
1555
|
-
* (`sourceManifestPath` present) already carry their own service directory.
|
|
1556
|
-
*
|
|
1557
|
-
* Exported for `assemble-producer-input.mjs`, which must derive the SAME
|
|
1558
|
-
* service directory to hash the SAME build-input closure the lock names.
|
|
1559
|
-
*/
|
|
1560
|
-
export function subjectServicePath(subject, sourceCommit, locate = FIRST_PARTY_TREES) {
|
|
1561
|
-
return subjectServiceSource(subject, sourceCommit, locate).path;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
/**
|
|
1565
|
-
* The repository-relative source directory of a subject and the kind of the
|
|
1566
|
-
* manifest component it is built from (`worker` for a worker component,
|
|
1567
|
-
* `service` otherwise — a legacy `ships.apis[]` entry or a subject with its own
|
|
1568
|
-
* source manifest is not a worker).
|
|
1569
|
-
*/
|
|
1570
|
-
function subjectServiceSource(subject, sourceCommit, locate = FIRST_PARTY_TREES) {
|
|
1571
|
-
const { dir: repoDirectory, base } = locate(subject.repo);
|
|
1572
|
-
const toRepoRelative = (absolute) =>
|
|
1573
|
-
relative(repoDirectory, absolute).split(sep).join('/') || '.';
|
|
1574
|
-
if (subject.sourceManifestPath) {
|
|
1575
|
-
return { path: toRepoRelative(join(base, subject.path)), componentKind: 'service', imageClass: undefined };
|
|
1576
|
-
}
|
|
1577
|
-
const biomePath = toRepoRelative(join(base, subject.path));
|
|
1578
|
-
const manifestPath =
|
|
1579
|
-
biomePath === '.' ? 'xema-biome.json' : `${biomePath}/xema-biome.json`;
|
|
1580
|
-
const manifest = readGitJsonAtCommit(
|
|
1581
|
-
repoDirectory,
|
|
1582
|
-
manifestPath,
|
|
1583
|
-
sourceCommit,
|
|
1584
|
-
);
|
|
1585
|
-
const xema = manifest?.xema;
|
|
1586
|
-
const apis = xema?.ships?.apis;
|
|
1587
|
-
const legacyApis = Array.isArray(apis) ? apis : [];
|
|
1588
|
-
// component-contract-v5: a component's source directory is declared by its
|
|
1589
|
-
// own `artifact.path`.
|
|
1590
|
-
//
|
|
1591
|
-
// Both `service` AND `worker` kinds qualify. Restricting this to `service`
|
|
1592
|
-
// was correct only while a runtime component was spelled in the legacy
|
|
1593
|
-
// `ships.apis[]` block; v5 dropped that field, so `opencode-worker` —
|
|
1594
|
-
// `kind: "worker"`, `scheduler: "runtime"` — fell through to the legacy
|
|
1595
|
-
// branch and failed with "ships no apis" even though its manifest declares
|
|
1596
|
-
// everything needed. What identifies the subject is
|
|
1597
|
-
// `protocol.serviceName`, not the component kind.
|
|
1598
|
-
const components = Array.isArray(xema?.components) ? xema.components : [];
|
|
1599
|
-
const v4ServiceComponents = components.filter(
|
|
1600
|
-
(component) =>
|
|
1601
|
-
component !== null &&
|
|
1602
|
-
typeof component === 'object' &&
|
|
1603
|
-
(component.kind === 'service' || component.kind === 'worker'),
|
|
1604
|
-
);
|
|
1605
|
-
if (legacyApis.length > 0 && v4ServiceComponents.length > 0) {
|
|
1606
|
-
throw new Error(
|
|
1607
|
-
`${subject.repo}@${sourceCommit}:${manifestPath} declares services in BOTH ` +
|
|
1608
|
-
`xema.ships.apis[] (legacy) and xema.components[] kind "service" ` +
|
|
1609
|
-
`(component-contract-v4) — a biome must use exactly one shape`,
|
|
1610
|
-
);
|
|
1611
|
-
}
|
|
1612
|
-
let servicePath;
|
|
1613
|
-
let componentKind = 'service';
|
|
1614
|
-
let imageClass;
|
|
1615
|
-
if (v4ServiceComponents.length > 0) {
|
|
1616
|
-
// A SERVICE is identified by `protocol.serviceName`; a WORKER has no
|
|
1617
|
-
// service protocol and is identified by its component `key`. Matching only
|
|
1618
|
-
// on `serviceName` silently found nothing for every worker subject and
|
|
1619
|
-
// reported it as an undeclared service directory.
|
|
1620
|
-
const component = v4ServiceComponents.find(
|
|
1621
|
-
(entry) =>
|
|
1622
|
-
entry?.protocol?.serviceName === subject.id || entry?.key === subject.id,
|
|
1623
|
-
);
|
|
1624
|
-
if (
|
|
1625
|
-
!component ||
|
|
1626
|
-
typeof component?.artifact?.path !== 'string' ||
|
|
1627
|
-
component.artifact.path.length === 0
|
|
1628
|
-
) {
|
|
1629
|
-
throw new Error(
|
|
1630
|
-
`${subject.repo}@${sourceCommit}:${manifestPath} declares no xema.components[] ` +
|
|
1631
|
-
`kind "service"/"worker" entry identified as "${subject.id}" (by ` +
|
|
1632
|
-
`protocol.serviceName or component key) with an ` +
|
|
1633
|
-
`artifact.path — the service source directory is the manifest's to declare ` +
|
|
1634
|
-
`and must never be synthesized`,
|
|
1635
|
-
);
|
|
1636
|
-
}
|
|
1637
|
-
servicePath = component.artifact.path;
|
|
1638
|
-
componentKind = component.kind;
|
|
1639
|
-
imageClass = component.imageClass;
|
|
1640
|
-
} else {
|
|
1641
|
-
if (legacyApis.length === 0) {
|
|
1642
|
-
throw new Error(
|
|
1643
|
-
`${subject.repo}@${sourceCommit}:${manifestPath} ships no apis, but runtime ` +
|
|
1644
|
-
`subject "${subject.kind}:${subject.id}" claims to be one of them`,
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
|
-
const api = legacyApis.find((entry) => entry?.name === subject.id);
|
|
1648
|
-
if (!api || typeof api.path !== 'string' || api.path.length === 0) {
|
|
1649
|
-
throw new Error(
|
|
1650
|
-
`${subject.repo}@${sourceCommit}:${manifestPath} declares no ships.apis[] entry ` +
|
|
1651
|
-
`named "${subject.id}" with a "path" — the service source directory is the ` +
|
|
1652
|
-
`manifest's to declare and must never be synthesized`,
|
|
1653
|
-
);
|
|
1654
|
-
}
|
|
1655
|
-
servicePath = api.path;
|
|
1656
|
-
}
|
|
1657
|
-
return { path: toRepoRelative(join(base, subject.path, servicePath)), componentKind, imageClass };
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
function assertSourceManifestParity(structure, commitByRepo, locate = FIRST_PARTY_TREES) {
|
|
1661
|
-
for (const source of structure.subjects) {
|
|
1662
|
-
const { dir: repoDirectory, base } = locate(source.repo);
|
|
1663
|
-
const sourceCommit = commitByRepo.get(source.repo);
|
|
1664
|
-
const manifestPath = source.sourceManifestPath
|
|
1665
|
-
? relative(repoDirectory, join(base, source.sourceManifestPath))
|
|
1666
|
-
.split(sep)
|
|
1667
|
-
.join('/')
|
|
1668
|
-
: relative(repoDirectory, join(base, source.path, 'xema-biome.json'))
|
|
1669
|
-
.split(sep)
|
|
1670
|
-
.join('/');
|
|
1671
|
-
const actual = exactSourceManifestHash(
|
|
1672
|
-
repoDirectory,
|
|
1673
|
-
manifestPath,
|
|
1674
|
-
sourceCommit,
|
|
1675
|
-
);
|
|
1676
|
-
if (actual !== source.sourceManifestHash) {
|
|
1677
|
-
throw new Error(
|
|
1678
|
-
`runtime subject "${source.kind}:${source.id}" sourceManifestHash does not match ${source.repo}@${sourceCommit}:${manifestPath}`,
|
|
1679
|
-
);
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
for (const biome of structure.webBiomes) {
|
|
1683
|
-
const { dir: repoDirectory, base } = locate(biome.repo);
|
|
1684
|
-
const sourceCommit = commitByRepo.get(biome.repo);
|
|
1685
|
-
const manifestPath = relative(
|
|
1686
|
-
repoDirectory,
|
|
1687
|
-
join(base, biome.path, 'xema-biome.json'),
|
|
1688
|
-
)
|
|
1689
|
-
.split(sep)
|
|
1690
|
-
.join('/');
|
|
1691
|
-
const actual = exactSourceManifestHash(
|
|
1692
|
-
repoDirectory,
|
|
1693
|
-
manifestPath,
|
|
1694
|
-
sourceCommit,
|
|
1695
|
-
);
|
|
1696
|
-
if (actual !== biome.sourceManifestHash) {
|
|
1697
|
-
throw new Error(
|
|
1698
|
-
`web biome "${biome.biomeId}" sourceManifestHash does not match ${biome.repo}@${sourceCommit}:${manifestPath}`,
|
|
1699
|
-
);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
/**
|
|
1705
|
-
* AN EXTERNAL SOURCE THAT VENDORS CROSS-REPOSITORY CONTENT IS REFUSED, LOUDLY.
|
|
1706
|
-
*
|
|
1707
|
-
* `runtime-image-source-index.json` declares the vendoring this fleet's own
|
|
1708
|
-
* repositories do, and an external source has no entry there — correctly, since
|
|
1709
|
-
* the index is the vendor's view of the vendor's repositories. Reading that
|
|
1710
|
-
* absence as "this repository vendors nothing" is the silent half: a Dockerfile
|
|
1711
|
-
* that copies a tree in from elsewhere would have those bytes drop out of the
|
|
1712
|
-
* closure, and the hash would name an image whose content it does not describe.
|
|
1713
|
-
*
|
|
1714
|
-
* So the ABSENCE is checked rather than assumed. The producing repository's own
|
|
1715
|
-
* committed record — the same file `vendor-cross-repo-inputs.mjs` reads to emit
|
|
1716
|
-
* the `--vendored` arguments CI hands the tag computation — is read at the
|
|
1717
|
-
* locked commit. Absent means genuinely nothing to vendor, which is the honest
|
|
1718
|
-
* empty. PRESENT means this plane cannot pin the other side (it has no checkout
|
|
1719
|
-
* of a repository the customer vendors from, and no authority to resolve one),
|
|
1720
|
-
* and an undeclared input must fail rather than silently drop out.
|
|
1721
|
-
*/
|
|
1722
|
-
function assertExternalSourcesDeclareNoVendoring(lock, locate, commitByRepo) {
|
|
1723
|
-
const external = new Set(
|
|
1724
|
-
(lock.biomes ?? [])
|
|
1725
|
-
.filter((biome) => biome.source !== undefined)
|
|
1726
|
-
.map((biome) => biome.repo),
|
|
1727
|
-
);
|
|
1728
|
-
for (const repo of [...external].sort(compareStrings)) {
|
|
1729
|
-
const { dir } = locate(repo);
|
|
1730
|
-
const commit = commitByRepo.get(repo);
|
|
1731
|
-
if (!commit) {continue;}
|
|
1732
|
-
// Existence is probed with `cat-file -e` rather than by catching the read:
|
|
1733
|
-
// a failed read writes `fatal: path ... does not exist` to stderr, and a
|
|
1734
|
-
// `fatal:` printed during a successful run is a false signal a reader has
|
|
1735
|
-
// to spend time discounting.
|
|
1736
|
-
let present;
|
|
1737
|
-
try {
|
|
1738
|
-
execFileSync('git', ['-C', dir, 'cat-file', '-e', `${commit}:${CROSS_REPO_BUILD_INPUTS_PATH}`], {
|
|
1739
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
1740
|
-
});
|
|
1741
|
-
present = true;
|
|
1742
|
-
} catch {
|
|
1743
|
-
present = false;
|
|
1744
|
-
}
|
|
1745
|
-
if (!present) {continue;} // absent — nothing vendored, which is the honest empty
|
|
1746
|
-
const record = readGitJsonAtCommit(dir, CROSS_REPO_BUILD_INPUTS_PATH, commit);
|
|
1747
|
-
const inputs = Array.isArray(record?.inputs) ? record.inputs : [];
|
|
1748
|
-
if (inputs.length === 0) {continue;}
|
|
1749
|
-
throw new Error(
|
|
1750
|
-
`external source "${repo}" declares ${inputs.length} cross-repository build ` +
|
|
1751
|
-
`input(s) in ${CROSS_REPO_BUILD_INPUTS_PATH} at ${commit}. This plane cannot pin ` +
|
|
1752
|
-
'the other side of them — it holds no checkout of the repositories they vendor ' +
|
|
1753
|
-
'from — so the image content hash would omit bytes the build bakes in and name a ' +
|
|
1754
|
-
'coordinate it does not describe. Refusing rather than hashing an incomplete ' +
|
|
1755
|
-
'closure.',
|
|
1756
|
-
);
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* The vendored cross-repository build inputs of THIS roster's images, keyed
|
|
1762
|
-
* `${consumerRepo}\0${consumerService ?? ''}`.
|
|
1763
|
-
*
|
|
1764
|
-
* EVERY COMMIT HERE COMES FROM A REPOSITORY PIN, NEVER FROM ROSTER MEMBERSHIP.
|
|
1765
|
-
*
|
|
1766
|
-
* The consumer commit — the commit whose committed
|
|
1767
|
-
* `tooling/release/cross-repo-build-inputs.json` names the vendored revisions —
|
|
1768
|
-
* is the consumer repository's `repositoryPins` entry. It used to be read out
|
|
1769
|
-
* of the edition's resolved commit map, which knows a repository only if some
|
|
1770
|
-
* SELECTED biome happens to come from it. That made an image's identity depend
|
|
1771
|
-
* on unrelated membership, and made a trimmed edition unaddressable: an edition
|
|
1772
|
-
* shipping no `repos/xema-community` biome refused with "cross-repo build inputs
|
|
1773
|
-
* are declared for repos/xema-community, but this distribution resolves no
|
|
1774
|
-
* commit for it" — although it shipped no xema-community image at all. For a
|
|
1775
|
-
* first-party subject the two are the same commit by construction (a subject's
|
|
1776
|
-
* `sourceCommit` IS its repository pin), so every edition that resolved before
|
|
1777
|
-
* resolves to byte-identical hashes now.
|
|
1778
|
-
*
|
|
1779
|
-
* Only consumers the roster actually BUILDS FROM are resolved: a consumer with
|
|
1780
|
-
* no selected subject contributes no input to any hash here, so reading its
|
|
1781
|
-
* record would be work that grades nothing. The vendored revision itself is the
|
|
1782
|
-
* consumer's own record at its pin (see the reasoning above the call site): the
|
|
1783
|
-
* pin fixes the record, and the record fixes the bytes. A consumer, or a
|
|
1784
|
-
* repository it vendors from, with NO repository pin is refused by name — the
|
|
1785
|
-
* image cannot be addressed without one.
|
|
1786
|
-
*/
|
|
1787
|
-
export function resolveCrossRepoInputsForRoster({
|
|
1788
|
-
runtimeSourceIndex,
|
|
1789
|
-
subjects,
|
|
1790
|
-
resolveVendored = resolveCrossRepoVendoredInputs,
|
|
1791
|
-
repoRoot = sourcesRoot(),
|
|
1792
|
-
// The subject kind that builds EVERY input of its repository (the composed
|
|
1793
|
-
// process image carries every service of its build). Undefined: none does.
|
|
1794
|
-
composedProcessKind = undefined,
|
|
1795
|
-
}) {
|
|
1796
|
-
const pins = new Map(
|
|
1797
|
-
runtimeSourceIndex.repositoryPins.map((pin) => [pin.repo, pin]),
|
|
1798
|
-
);
|
|
1799
|
-
const requirePin = (repo, role) => {
|
|
1800
|
-
const pin = pins.get(repo);
|
|
1801
|
-
if (!pin) {
|
|
1802
|
-
throw new Error(
|
|
1803
|
-
`cross-repo build inputs need a repository pin for "${repo}" (${role}), ` +
|
|
1804
|
-
'and runtime-image-source-index.json declares none — the vendored content ' +
|
|
1805
|
-
'cannot be pinned, so the image cannot be addressed',
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
return pin;
|
|
1809
|
-
};
|
|
1810
|
-
const builds = (input) =>
|
|
1811
|
-
subjects.some(
|
|
1812
|
-
(subject) =>
|
|
1813
|
-
subject.repo === input.consumerRepo &&
|
|
1814
|
-
(input.consumerService === undefined || input.consumerService === null ||
|
|
1815
|
-
input.consumerService === subject.id ||
|
|
1816
|
-
(composedProcessKind !== undefined && subject.kind === composedProcessKind)),
|
|
1817
|
-
);
|
|
1818
|
-
const needed = runtimeSourceIndex.crossRepoBuildInputs.filter(builds);
|
|
1819
|
-
const vendoredCommitByInputKey = new Map();
|
|
1820
|
-
for (const consumerRepo of [
|
|
1821
|
-
...new Set(needed.map((input) => input.consumerRepo)),
|
|
1822
|
-
].sort(compareStrings)) {
|
|
1823
|
-
const consumerCommit = requirePin(consumerRepo, 'the consuming repository').sourceCommit;
|
|
1824
|
-
for (const subject of subjects) {
|
|
1825
|
-
if (subject.repo === consumerRepo && subject.sourceCommit !== undefined &&
|
|
1826
|
-
subject.sourceCommit !== consumerCommit) {
|
|
1827
|
-
throw new Error(
|
|
1828
|
-
`runtime subject "${subject.kind}:${subject.id}" is resolved at ` +
|
|
1829
|
-
`${subject.sourceCommit}, but "${consumerRepo}" is pinned at ${consumerCommit}`,
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
// The record is read against EVERY input the index declares for this
|
|
1834
|
-
// consumer, not the roster-filtered subset: set equality with the
|
|
1835
|
-
// consumer's record is the fence against a drifted declaration, and it must
|
|
1836
|
-
// not weaken because an edition ships fewer services.
|
|
1837
|
-
for (const resolved of resolveVendored({
|
|
1838
|
-
indexInputs: runtimeSourceIndex.crossRepoBuildInputs,
|
|
1839
|
-
consumerRepo,
|
|
1840
|
-
consumerRepoDir: join(repoRoot, consumerRepo),
|
|
1841
|
-
consumerCommit,
|
|
1842
|
-
})) {
|
|
1843
|
-
vendoredCommitByInputKey.set(
|
|
1844
|
-
consumerInputKey(consumerRepo, {
|
|
1845
|
-
sourceRepo: relative(repoRoot, resolved.sourceRepoDir),
|
|
1846
|
-
sourcePath: resolved.sourcePath,
|
|
1847
|
-
destinationPath: resolved.destinationPath,
|
|
1848
|
-
}),
|
|
1849
|
-
resolved.sourceCommit,
|
|
1850
|
-
);
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
const crossRepoInputsByConsumer = new Map();
|
|
1854
|
-
for (const input of needed) {
|
|
1855
|
-
requirePin(input.sourceRepo, `vendored into "${input.consumerRepo}"`);
|
|
1856
|
-
// A null `consumerService` means the vendoring step runs for EVERY service
|
|
1857
|
-
// built from that repository; it is keyed on the repository alone.
|
|
1858
|
-
const key = `${input.consumerRepo}\u0000${input.consumerService ?? ''}`;
|
|
1859
|
-
const sourceCommit = vendoredCommitByInputKey.get(
|
|
1860
|
-
consumerInputKey(input.consumerRepo, input),
|
|
1861
|
-
);
|
|
1862
|
-
if (!sourceCommit) {
|
|
1863
|
-
throw new Error(
|
|
1864
|
-
`cross-repo build input for "${input.consumerService}" vendoring ` +
|
|
1865
|
-
`"${input.sourceRepo}:${input.sourcePath}" has no commit in ` +
|
|
1866
|
-
`"${input.consumerRepo}"'s own cross-repo build-input record — its ` +
|
|
1867
|
-
`vendored content cannot be pinned, so the image cannot be addressed`,
|
|
1868
|
-
);
|
|
1869
|
-
}
|
|
1870
|
-
const entries = crossRepoInputsByConsumer.get(key) ?? [];
|
|
1871
|
-
entries.push({
|
|
1872
|
-
destinationPath: input.destinationPath,
|
|
1873
|
-
sourceRepoDir: join(repoRoot, input.sourceRepo),
|
|
1874
|
-
sourcePath: input.sourcePath,
|
|
1875
|
-
sourceCommit,
|
|
1876
|
-
});
|
|
1877
|
-
crossRepoInputsByConsumer.set(key, entries);
|
|
1878
|
-
}
|
|
1879
|
-
return crossRepoInputsByConsumer;
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
/**
|
|
1883
|
-
* The release's inputs: the COMMITTED lock, and the edition's own indexes
|
|
1884
|
-
* (`editionPinnedInputs`: the committed ones, or rebuilt at the repositories the
|
|
1885
|
-
* edition holds).
|
|
1886
|
-
*/
|
|
1887
|
-
async function releaseImageInputs(schemas, id) {
|
|
1888
|
-
const pinned = await editionPinnedInputs(id, { schemas });
|
|
1889
|
-
return {
|
|
1890
|
-
lock: validateImmutableDistributionLock(
|
|
1891
|
-
readBoundDistributionFile({
|
|
1892
|
-
distributionsRoot: editionsRoot(),
|
|
1893
|
-
distributionId: id,
|
|
1894
|
-
fileName: 'distribution.lock.json',
|
|
1895
|
-
// A pure READ of the committed lock: the tolerant reader, never the writer.
|
|
1896
|
-
parse: (raw) => parseResolvedDistributionLock(schemas, raw),
|
|
1897
|
-
}).value,
|
|
1898
|
-
),
|
|
1899
|
-
runtimeSourceIndex: pinned.runtimeImageSourceIndex,
|
|
1900
|
-
biomeIndex: pinned.biomeIndex,
|
|
1901
|
-
requiredPlatforms: resolveDistributionRequiredPlatforms(schemas, id),
|
|
1902
|
-
declaredSources: resolveDistributionDeclaredSources(schemas, id),
|
|
1903
|
-
};
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
/**
|
|
1907
|
-
* A develop snapshot's inputs: the SNAPSHOT distribution lock and the
|
|
1908
|
-
* snapshot indexes and chain (`develop-snapshot-inputs.mjs`), all already
|
|
1909
|
-
* resolved at the snapshot's pins. The runtime index carries the snapshot pins
|
|
1910
|
-
* already and is not re-pinned here.
|
|
1911
|
-
*/
|
|
1912
|
-
function snapshotImageInputs(schemas, { inputs, lock }) {
|
|
1913
|
-
return {
|
|
1914
|
-
lock: validateImmutableDistributionLock(lock),
|
|
1915
|
-
runtimeSourceIndex: inputs.runtimeImageSourceIndex,
|
|
1916
|
-
biomeIndex: inputs.biomeIndex,
|
|
1917
|
-
requiredPlatforms: schemas.resolveRuntimeImagePlatforms(inputs.composed.requiredPlatforms),
|
|
1918
|
-
declaredSources: inputs.composed.declaredSources,
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
/**
|
|
1923
|
-
* The supplemental source declarations an image resolve reads beside
|
|
1924
|
-
* `runtimeSourceIndex`. An index with no supplemental subjects (a customer
|
|
1925
|
-
* edition's own rows) needs none, and its workspace need not declare the
|
|
1926
|
-
* section; any other reads the edition workspace's.
|
|
1927
|
-
*/
|
|
1928
|
-
export function supplementalSourcesFor(runtimeSourceIndex) {
|
|
1929
|
-
return runtimeSourceIndex.supplementalSubjects.length === 0
|
|
1930
|
-
? []
|
|
1931
|
-
: workspaceSection('runtimeImageSources', 'resolving supplemental image subjects').supplementalSources;
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
/**
|
|
1935
|
-
* The image ROWS of one edition: its subjects (hashed at the commits the lock
|
|
1936
|
-
* pins), its platform-service bindings and its bundled web biomes, after every
|
|
1937
|
-
* check that needs no parsed lock. Rows are not a lock: the kernel's image-lock
|
|
1938
|
-
* grammar is the grammar of a COMPLETE lock, and a caller that holds only part
|
|
1939
|
-
* of one (a customer's own rows, merged with the catalogue's) parses the lock
|
|
1940
|
-
* it completes. `resolveImages` is these rows parsed as the whole lock.
|
|
1941
|
-
*/
|
|
1942
|
-
export async function resolveImageRows(id, { inputs, lock, onSubjectHashed, recordedImages, previousLock = null } = {}) {
|
|
1943
|
-
assertCanonicalDistributionId(id);
|
|
1944
|
-
if ((inputs === undefined) !== (lock === undefined)) {
|
|
1945
|
-
throw new Error('resolveImages takes the snapshot `inputs` and its distribution `lock` together, or neither');
|
|
1946
|
-
}
|
|
1947
|
-
// A build record (images-from-record.mjs) replaces the hashing on the snapshot lane only, and never beside a probe.
|
|
1948
|
-
if (recordedImages !== undefined && (inputs === undefined || onSubjectHashed !== undefined)) {
|
|
1949
|
-
throw new Error('resolveImages takes recorded images only on the snapshot lane, and never with onSubjectHashed');
|
|
1950
|
-
}
|
|
1951
|
-
const schemas = await loadKernelDistributionSchemas();
|
|
1952
|
-
const {
|
|
1953
|
-
lock: distributionLock,
|
|
1954
|
-
runtimeSourceIndex,
|
|
1955
|
-
biomeIndex,
|
|
1956
|
-
requiredPlatforms,
|
|
1957
|
-
declaredSources,
|
|
1958
|
-
} = inputs === undefined ? await releaseImageInputs(schemas, id) : snapshotImageInputs(schemas, { inputs, lock });
|
|
1959
|
-
// Materialise every EXTERNAL source at the commit the lock pins, and resolve
|
|
1960
|
-
// every `repo` handle through one locator from here down.
|
|
1961
|
-
const locate = sourceTreeLocator(externalSourceTrees(distributionLock));
|
|
1962
|
-
const structure = resolveComposedProcessStructure(
|
|
1963
|
-
distributionLock,
|
|
1964
|
-
resolveImageStructure(
|
|
1965
|
-
distributionLock,
|
|
1966
|
-
runtimeSourceIndex,
|
|
1967
|
-
schemas,
|
|
1968
|
-
requiredPlatforms,
|
|
1969
|
-
declaredSources,
|
|
1970
|
-
{
|
|
1971
|
-
biomeIndex,
|
|
1972
|
-
supplementalSources: supplementalSourcesFor(runtimeSourceIndex),
|
|
1973
|
-
thirdPartyPins: loadThirdPartySourcePins(dataRoot()),
|
|
1974
|
-
},
|
|
1975
|
-
),
|
|
1976
|
-
schemas,
|
|
1977
|
-
requiredPlatforms,
|
|
1978
|
-
{ locate },
|
|
1979
|
-
);
|
|
1980
|
-
const commitsBefore = sourceCommitMap(structure, locate);
|
|
1981
|
-
assertSourceManifestParity(structure, commitsBefore, locate);
|
|
1982
|
-
assertExternalSourcesDeclareNoVendoring(distributionLock, locate, commitsBefore);
|
|
1983
|
-
|
|
1984
|
-
const buildStepDirectoriesByRepo = new Map();
|
|
1985
|
-
for (const directory of runtimeSourceIndex.buildStepContextDirectories) {
|
|
1986
|
-
const entries = buildStepDirectoriesByRepo.get(directory.repo) ?? [];
|
|
1987
|
-
entries.push(directory.path);
|
|
1988
|
-
buildStepDirectoriesByRepo.set(directory.repo, entries);
|
|
1989
|
-
}
|
|
1990
|
-
// Vendored content is pinned from the CONSUMER REPOSITORY'S OWN committed
|
|
1991
|
-
// record of what it vendored (`resolveCrossRepoVendoredInputs`) — never from
|
|
1992
|
-
// the index's `repositoryPins` mirror of the vendored axis, and never from this
|
|
1993
|
-
// edition's resolved commit map. The record is read at the CONSUMER's
|
|
1994
|
-
// repository pin, which is what makes the result roster-independent (see
|
|
1995
|
-
// `resolveCrossRepoInputsForRoster`).
|
|
1996
|
-
//
|
|
1997
|
-
// An image's identity is edition-INDEPENDENT, and that still holds: the record
|
|
1998
|
-
// is a property of the consumer's COMMIT, not of any edition, so every edition
|
|
1999
|
-
// shipping a subject derives the same `contentHash` for it.
|
|
2000
|
-
// The first-party `biome-host-api` image is built once, by the producing
|
|
2001
|
-
// repository's CI, which has no notion of an edition and vendors every
|
|
2002
|
-
// declared cross-repo input. Reading the vendored commit from `commitsBefore`
|
|
2003
|
-
// made that edition-independent quantity depend on the edition, which failed
|
|
2004
|
-
// in both directions at once:
|
|
2005
|
-
// • it is UNCOMPUTABLE for an edition that does not itself resolve the
|
|
2006
|
-
// source repo — `appliance-lean` does not ship the xema-store
|
|
2007
|
-
// biome, so re-resolving it threw here and their locks could not
|
|
2008
|
-
// be regenerated at all, even though the image they name is the very one
|
|
2009
|
-
// the other six editions resolve fine;
|
|
2010
|
-
// • had it not thrown it would have been WORSE, silently minting a second
|
|
2011
|
-
// content hash for one image — a coordinate no CI ever pushes, which is
|
|
2012
|
-
// the exact "the tag the lock names does not exist" failure the delivery
|
|
2013
|
-
// chain exists to prevent (.claude/rules/image-release-coordinate.md).
|
|
2014
|
-
//
|
|
2015
|
-
// Reading `index.repositoryPins` instead was the SAME failure by a different
|
|
2016
|
-
// route, and it shipped. The mirror is generated from the index and committed
|
|
2017
|
-
// separately, so lagging it by a pin is its normal resting state — and the
|
|
2018
|
-
// vendored commit is framed into the image-input hash's component KEY, so a
|
|
2019
|
-
// one-commit lag mints a `contentHash` for byte-identical bytes that no CI run
|
|
2020
|
-
// has ever pushed. Measured 2026-09-07 on `origin/main`: five of sixty-eight
|
|
2021
|
-
// coordinates 404'd for exactly this, while the tags CI DID push were served
|
|
2022
|
-
// on both platforms. The gate that was supposed to catch it,
|
|
2023
|
-
// `tooling/release/check-image-tag-parity.mjs` in the AGGREGATOR (a path that
|
|
2024
|
-
// resolves in no producing repository, though two of them cite it as if it
|
|
2025
|
-
// did), read `index.repositoryPins` too — so both of its halves consumed the
|
|
2026
|
-
// one input that had diverged, and it reported 474/474 verified while the
|
|
2027
|
-
// delivery plane was undeliverable. It now reads the consumer's record, as
|
|
2028
|
-
// this does.
|
|
2029
|
-
const crossRepoInputsByConsumer = resolveCrossRepoInputsForRoster({
|
|
2030
|
-
runtimeSourceIndex,
|
|
2031
|
-
subjects: structure.subjects,
|
|
2032
|
-
composedProcessKind: schemas.RuntimeImageSubjectKind.ComposedProcess,
|
|
2033
|
-
});
|
|
2034
|
-
// A composed image carries EVERY service of its build, so it vendors every
|
|
2035
|
-
// cross-repository input its repository declares — repository-wide and
|
|
2036
|
-
// per-service alike. The set is a property of the repository at its commit,
|
|
2037
|
-
// never of the edition, so every edition derives the same tag for it.
|
|
2038
|
-
const crossRepoInputsFor = (subject) =>
|
|
2039
|
-
subject.kind === schemas.RuntimeImageSubjectKind.ComposedProcess
|
|
2040
|
-
? [...crossRepoInputsByConsumer]
|
|
2041
|
-
.filter(([key]) => key.startsWith(`${subject.repo}\u0000`))
|
|
2042
|
-
.sort(([a], [b]) => compareStrings(a, b))
|
|
2043
|
-
.flatMap(([, entries]) => entries)
|
|
2044
|
-
: [
|
|
2045
|
-
...(crossRepoInputsByConsumer.get(`${subject.repo}\u0000`) ?? []),
|
|
2046
|
-
...(crossRepoInputsByConsumer.get(`${subject.repo}\u0000${subject.id}`) ?? []),
|
|
2047
|
-
];
|
|
2048
|
-
const subjects = [];
|
|
2049
|
-
for (const subject of structure.subjects) {
|
|
2050
|
-
const sourceCommit = commitsBefore.get(subject.repo);
|
|
2051
|
-
const repoDirectory = locate(subject.repo).dir;
|
|
2052
|
-
const servicePath = subjectServicePath(subject, sourceCommit, locate);
|
|
2053
|
-
const { sourceManifestPath: ignoredSourceManifestPath, ...lockedSubject } =
|
|
2054
|
-
subject;
|
|
2055
|
-
void ignoredSourceManifestPath;
|
|
2056
|
-
// With a build record the content hash and source commit are the record's, carried and never recomputed.
|
|
2057
|
-
const isComposed = subject.kind === schemas.RuntimeImageSubjectKind.ComposedProcess;
|
|
2058
|
-
const hashed = recordedImages === undefined ? {
|
|
2059
|
-
...lockedSubject,
|
|
2060
|
-
sourceCommit,
|
|
2061
|
-
// The hashing lane computes each member's key itself: the member service's own image-input hash.
|
|
2062
|
-
...(isComposed ? {
|
|
2063
|
-
members: composedMemberKeys({
|
|
2064
|
-
subject,
|
|
2065
|
-
repoDirectory,
|
|
2066
|
-
sourceCommit,
|
|
2067
|
-
crossRepoInputsByConsumer,
|
|
2068
|
-
buildStepDirectories: buildStepDirectoriesByRepo.get(subject.repo) ?? [],
|
|
2069
|
-
}),
|
|
2070
|
-
} : {}),
|
|
2071
|
-
contentHash: exactImageBuildInputHash(
|
|
2072
|
-
{
|
|
2073
|
-
repoDir: repoDirectory,
|
|
2074
|
-
service: subject.id,
|
|
2075
|
-
servicePath,
|
|
2076
|
-
commit: sourceCommit,
|
|
2077
|
-
crossRepoInputs: crossRepoInputsFor(subject),
|
|
2078
|
-
buildStepDirectories:
|
|
2079
|
-
buildStepDirectoriesByRepo.get(subject.repo) ?? [],
|
|
2080
|
-
},
|
|
2081
|
-
imageInputSourceHashAlgorithm(),
|
|
2082
|
-
),
|
|
2083
|
-
} : { ...lockedSubject, ...recordedImages.take({ ...lockedSubject, sourceCommit }) };
|
|
2084
|
-
subjects.push(hashed);
|
|
2085
|
-
// The hash is the last thing a registry probe waits for: hand the subject
|
|
2086
|
-
// over now, and let the event loop run so the probes of the subjects
|
|
2087
|
-
// already hashed progress while the rest hash (hashing is synchronous).
|
|
2088
|
-
if (onSubjectHashed !== undefined) {
|
|
2089
|
-
onSubjectHashed(hashed);
|
|
2090
|
-
await yieldToEventLoop();
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
// Every subject missing from, contradicted by or extra in the build record, refused at once.
|
|
2094
|
-
recordedImages?.settle();
|
|
2095
|
-
// The composed artifacts are sealed with their generation: the roster's composed services must all be built, then the
|
|
2096
|
-
// kernel carries the previous lock's still-pinned artifacts forward (`previousLock` null or a v4 lock: generation 1).
|
|
2097
|
-
const ComposedProcess = schemas.RuntimeImageSubjectKind.ComposedProcess;
|
|
2098
|
-
const composedBuilt = subjects.filter((subject) => subject.kind === ComposedProcess);
|
|
2099
|
-
assertBuiltCoversEditionRoster({
|
|
2100
|
-
rosterServices: editionRosterServices(distributionLock),
|
|
2101
|
-
serviceSubjects: [...subjects, ...structure.thirdPartySubjects].filter((subject) => subject.kind === schemas.RuntimeImageSubjectKind.Service),
|
|
2102
|
-
composedBuilt,
|
|
2103
|
-
});
|
|
2104
|
-
if (composedBuilt.length > 0 || previousLock !== null) {
|
|
2105
|
-
const sealed = schemas.carryForwardComposedArtifacts({ previous: previousLock, built: composedBuilt });
|
|
2106
|
-
subjects.splice(0, subjects.length, ...subjects.filter((subject) => subject.kind !== ComposedProcess), ...sealed);
|
|
2107
|
-
subjects.sort(compareSubject);
|
|
2108
|
-
}
|
|
2109
|
-
// Release lane only: a develop snapshot or customer overlay is not covered by
|
|
2110
|
-
// the committed ledger.
|
|
2111
|
-
if (inputs === undefined) {
|
|
2112
|
-
assertBuildLedgerComplete({
|
|
2113
|
-
ledger: readBuildLedger(),
|
|
2114
|
-
distributionId: id,
|
|
2115
|
-
subjects,
|
|
2116
|
-
requiredPlatforms,
|
|
2117
|
-
expectedAt: join(dataRoot(), BUILD_LEDGER_FILE),
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
const webBiomes = structure.webBiomes.map((biome) => ({
|
|
2121
|
-
...biome,
|
|
2122
|
-
sourceCommit: commitsBefore.get(biome.repo),
|
|
2123
|
-
}));
|
|
2124
|
-
// The SAME locator the hash used. The default locator resolves every repo
|
|
2125
|
-
// handle to a first-party tree under the aggregator root, so for an edition
|
|
2126
|
-
// with an external source it looked for `<sourcesRoot()>/<source name>`, found no
|
|
2127
|
-
// repository there, and refused with "absent from the local object database"
|
|
2128
|
-
// — after every hash had already been computed correctly through `locate`.
|
|
2129
|
-
// Measured 2026-09-16 resolving a customer edition against a customer root.
|
|
2130
|
-
const commitsAfter = sourceCommitMap(structure, locate);
|
|
2131
|
-
if (serializeJson([...commitsBefore]) !== serializeJson([...commitsAfter])) {
|
|
2132
|
-
throw new Error(
|
|
2133
|
-
'refusing image-lock write: a source repository commit changed while hashing',
|
|
2134
|
-
);
|
|
2135
|
-
}
|
|
2136
|
-
// After the ledger check and the hashing: a third-party subject has no ledger row and no checkout, and its
|
|
2137
|
-
// `contentHash` is the publisher's statement, so none of the above read it.
|
|
2138
|
-
return {
|
|
2139
|
-
subjects: structure.thirdPartySubjects.length === 0 ? subjects : [...subjects, ...structure.thirdPartySubjects].sort(compareSubject),
|
|
2140
|
-
thirdPartySubjects: structure.thirdPartySubjects,
|
|
2141
|
-
platformServiceBindings: structure.platformServiceBindings,
|
|
2142
|
-
webBiomes,
|
|
2143
|
-
contractAuthority: runtimeSourceIndex.contractAuthority,
|
|
2144
|
-
commits: [...commitsBefore],
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
export const PREVIOUS_SEAL_USAGE =
|
|
2149
|
-
'exactly one of `--previous-seal-signer <certificate identity>` (the CLI reads the images.lock layer of the latest edition ' +
|
|
2150
|
-
'signed by that identity in this edition\'s registry repository; run under the per-edition concurrency group), ' +
|
|
2151
|
-
'`--previous-lock <images.lock.json> --previous-lock-sha256 <hex>` (the same layer, taken by the caller) or `--first-seal` ' +
|
|
2152
|
-
'(only for a distribution that has never had an edition)';
|
|
2153
|
-
|
|
2154
|
-
/**
|
|
2155
|
-
* The previous seal an images-lock write carries composed artifacts forward from, as an EXPLICIT input of exactly one kind:
|
|
2156
|
-
* - `registry` (`--previous-seal-signer`): `{ find }`, where `find()` returns the latest signed edition's images.lock bytes
|
|
2157
|
-
* and their sha256 (previous-seal-registry.mjs);
|
|
2158
|
-
* - the previous lock with the sha256 of its bytes (taken by the caller from the signed, published edition's images.lock
|
|
2159
|
-
* layer);
|
|
2160
|
-
* - `--first-seal` (null: generation 1).
|
|
2161
|
-
* None, more than one kind, or half of the pair is refused; the bytes must hash to the given digest, and the lock is parsed
|
|
2162
|
-
* with the kernel's `parseImageLock` and must be this edition's own (`distributionId`).
|
|
2163
|
-
*/
|
|
2164
|
-
export async function resolvePreviousSeal({ distributionId, previousLockPath, previousLockSha256, firstSeal = false, registry = undefined }) {
|
|
2165
|
-
const given = [previousLockPath, previousLockSha256].filter((value) => value !== undefined);
|
|
2166
|
-
const kinds = [registry !== undefined, given.length > 0, firstSeal].filter(Boolean).length;
|
|
2167
|
-
if (kinds > 1) {
|
|
2168
|
-
throw new Error(`more than one previous seal is given (--previous-seal-signer, --previous-lock/--previous-lock-sha256, --first-seal); give ${PREVIOUS_SEAL_USAGE}`);
|
|
2169
|
-
}
|
|
2170
|
-
if (firstSeal) {return null;}
|
|
2171
|
-
if (registry !== undefined) {
|
|
2172
|
-
const { bytes, imagesLockSha256, edition } = await registry.find();
|
|
2173
|
-
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
2174
|
-
if (actual !== imagesLockSha256) {
|
|
2175
|
-
throw new Error(`the images lock of ${edition} has sha256 ${actual}, not its layer digest ${imagesLockSha256}`);
|
|
2176
|
-
}
|
|
2177
|
-
return parsePreviousLock(distributionId, parseStrictJson(bytes.toString('utf8'), `${edition} images.lock.json`), `the images lock of ${edition}`);
|
|
2178
|
-
}
|
|
2179
|
-
if (given.length !== 2) {
|
|
2180
|
-
throw new Error(
|
|
2181
|
-
given.length === 0
|
|
2182
|
-
? `no previous seal is given; give ${PREVIOUS_SEAL_USAGE}`
|
|
2183
|
-
: `--previous-lock and --previous-lock-sha256 go together (one was given); give ${PREVIOUS_SEAL_USAGE}`,
|
|
2184
|
-
);
|
|
2185
|
-
}
|
|
2186
|
-
const bytes = readPreviousLockBytes(previousLockPath, previousLockSha256);
|
|
2187
|
-
return parsePreviousLock(distributionId, parseStrictJson(bytes.toString('utf8'), previousLockPath), '--previous-lock');
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
/**
|
|
2191
|
-
* The bytes of a previous lock file, held to the sha256 (hex) they were given with: the one check of the pair
|
|
2192
|
-
* (`flags` names the pair in the refusal). The parse, with the kernel and against the edition's id, is the caller's.
|
|
2193
|
-
*/
|
|
2194
|
-
export function readPreviousLockBytes(path, sha256, flags = { lock: '--previous-lock', sha256: '--previous-lock-sha256' }) {
|
|
2195
|
-
const bytes = readFileSync(path);
|
|
2196
|
-
const actual = createHash('sha256').update(bytes).digest('hex');
|
|
2197
|
-
if (actual !== sha256) {
|
|
2198
|
-
throw new Error(`${flags.lock} ${path} has sha256 ${actual}, not the given ${flags.sha256} ${sha256}`);
|
|
2199
|
-
}
|
|
2200
|
-
return bytes;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
/** The in-repo lane's previous lock: the committed file at `outPath`, parsed before it is overwritten; null (first seal) when there is none. */
|
|
2204
|
-
export async function readCommittedPreviousLock(distributionId, outPath) {
|
|
2205
|
-
return existsSync(outPath) ? parsePreviousLock(distributionId, readStrictJsonFile(outPath), outPath) : null;
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
async function parsePreviousLock(distributionId, document, label) {
|
|
2209
|
-
const previous = (await loadKernelDistributionSchemas()).parseImageLock(document);
|
|
2210
|
-
if (previous.distributionId !== distributionId) {
|
|
2211
|
-
throw new Error(`${label} is the images lock of "${previous.distributionId}", not of "${distributionId}"`);
|
|
2212
|
-
}
|
|
2213
|
-
return previous;
|
|
2214
|
-
}
|
|
2215
|
-
|
|
2216
|
-
/**
|
|
2217
|
-
* The composed artifacts of a STRUCTURAL view (no source hashing), carried forward from `previousLock` through the kernel's own
|
|
2218
|
-
* `carryForwardComposedArtifacts`, exactly as a reseal does. A structural subject has no content hash or member keys; each
|
|
2219
|
-
* composed id takes them from its newest previous artifact (a structure the previous lock does not cover keeps none, and the
|
|
2220
|
-
* kernel refuses it: the caller reports that as out of date). Source drift is `--check-sources`' job, through `resolveImages`.
|
|
2221
|
-
*/
|
|
2222
|
-
export function carryComposedForwardStructurally({ subjects, previousLock, schemas }) {
|
|
2223
|
-
const ComposedProcess = schemas.RuntimeImageSubjectKind.ComposedProcess;
|
|
2224
|
-
const newest = new Map();
|
|
2225
|
-
for (const subject of previousLock?.subjects ?? []) {
|
|
2226
|
-
if (subject.kind === ComposedProcess && (newest.get(subject.id)?.generation ?? 0) <= (subject.generation ?? 0)) {newest.set(subject.id, subject);}
|
|
2227
|
-
}
|
|
2228
|
-
const built = subjects.filter((subject) => subject.kind === ComposedProcess).map((subject) => {
|
|
2229
|
-
const previous = newest.get(subject.id);
|
|
2230
|
-
if (previous === undefined) {return subject;}
|
|
2231
|
-
const keyOf = new Map((previous.members ?? []).map((member) => [member.service, member.key]));
|
|
2232
|
-
return {
|
|
2233
|
-
...subject,
|
|
2234
|
-
contentHash: previous.contentHash,
|
|
2235
|
-
members: (subject.members ?? []).map((member) => (keyOf.has(member.service) ? { ...member, key: keyOf.get(member.service) } : member)),
|
|
2236
|
-
};
|
|
2237
|
-
});
|
|
2238
|
-
if (built.length === 0 && previousLock === null) {return subjects;}
|
|
2239
|
-
const sealed = schemas.carryForwardComposedArtifacts({ previous: previousLock, built });
|
|
2240
|
-
return [...subjects.filter((subject) => subject.kind !== ComposedProcess), ...sealed].sort(compareSubject);
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
/** One edition's COMPLETE image lock: its rows, parsed and validated as the kernel's image lock. */
|
|
2244
|
-
export async function resolveImages(id, { inputs, lock, onSubjectHashed, recordedImages, previousLock = null } = {}) {
|
|
2245
|
-
const { subjects, thirdPartySubjects, platformServiceBindings, webBiomes, contractAuthority, commits } = await resolveImageRows(id, { inputs, lock, onSubjectHashed, recordedImages, previousLock });
|
|
2246
|
-
const schemas = await loadKernelDistributionSchemas();
|
|
2247
|
-
const imageLock = validateImmutableImageLock(
|
|
2248
|
-
schemas.parseImageLock({
|
|
2249
|
-
schemaVersion: imageLockWriteSchemaVersion(schemas, subjects, `distribution "${id}"`),
|
|
2250
|
-
sourceHashAlgorithm: schemas.ImageSourceHashAlgorithm.ImageInputV2,
|
|
2251
|
-
distributionId: id,
|
|
2252
|
-
contractAuthority,
|
|
2253
|
-
subjects,
|
|
2254
|
-
platformServiceBindings,
|
|
2255
|
-
webBiomes,
|
|
2256
|
-
}),
|
|
2257
|
-
schemas,
|
|
2258
|
-
);
|
|
2259
|
-
return { imageLock, commits, thirdPartySubjects };
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
/**
|
|
2263
|
-
* Resolve and persist one schema-v3 image lock. Keeping the explicit trusted
|
|
2264
|
-
* write acknowledgement on this exported boundary means the in-process plane
|
|
2265
|
-
* refresh has the same safety contract as the standalone CLI.
|
|
2266
|
-
*/
|
|
2267
|
-
export async function writeResolvedImages(
|
|
2268
|
-
id,
|
|
2269
|
-
{ trustedWriteV3 = false, log = true, previousLockPath = undefined, previousLockSha256 = undefined, firstSeal = false, previousFromTarget = false } = {},
|
|
2270
|
-
) {
|
|
2271
|
-
assertCanonicalDistributionId(id);
|
|
2272
|
-
if (!trustedWriteV3) {
|
|
2273
|
-
throw new Error(
|
|
2274
|
-
'refusing to write a schema-v3 runtime image roster without --trusted-write-v3',
|
|
2275
|
-
);
|
|
2276
|
-
}
|
|
2277
|
-
// The RELEASE lock is published-only: a develop edition resolves through
|
|
2278
|
-
// `develop-edition.mjs` and never reaches this writer.
|
|
2279
|
-
const outPath = distributionOutputPath(
|
|
2280
|
-
editionsRoot(),
|
|
2281
|
-
id,
|
|
2282
|
-
'images.lock.json',
|
|
2283
|
-
);
|
|
2284
|
-
// The in-repo lane: the previous lock is the committed file at the target path (git is its authority), read before it is
|
|
2285
|
-
// overwritten; with no file there, this is the first seal.
|
|
2286
|
-
const previousLock = previousFromTarget
|
|
2287
|
-
? await readCommittedPreviousLock(id, outPath)
|
|
2288
|
-
: await resolvePreviousSeal({ distributionId: id, previousLockPath, previousLockSha256, firstSeal });
|
|
2289
|
-
if (previousFromTarget && previousLock === null && log) {console.log(`[dist:images] ${id}: first seal: no previous images.lock.json`);}
|
|
2290
|
-
const { imageLock, commits, thirdPartySubjects } = await resolveImages(id, { previousLock });
|
|
2291
|
-
assertReleaseContent(imageLock, `the images lock for "${id}"`);
|
|
2292
|
-
writeFileSync(outPath, serializeJson(imageLock));
|
|
2293
|
-
if (log) {
|
|
2294
|
-
console.log(
|
|
2295
|
-
`[dist:images] wrote ${outPath} — ${imageLock.subjects.length} actual runtime image subjects, ${imageLock.webBiomes.length} bundled web biomes, ${imageLock.platformServiceBindings.length} platform bindings.`,
|
|
2296
|
-
);
|
|
2297
|
-
for (const [repo, commit] of commits) {
|
|
2298
|
-
console.log(` source ${repo}@${commit}`);
|
|
2299
|
-
}
|
|
2300
|
-
for (const subject of thirdPartySubjects) {
|
|
2301
|
-
console.log(` third-party ${subject.kind}:${subject.id} ${subject.imageRef} ${attestedContentHash(subject)}`);
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
return { imageLock, commits };
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
async function main() {
|
|
2308
|
-
const id = process.argv[2];
|
|
2309
|
-
const trustedWriteV3 = process.argv.includes('--trusted-write-v3');
|
|
2310
|
-
if (!id) {
|
|
2311
|
-
throw new Error(
|
|
2312
|
-
`usage: resolve-images.mjs <distribution-id> --trusted-write-v3 (${PREVIOUS_SEAL_USAGE})`,
|
|
2313
|
-
);
|
|
2314
|
-
}
|
|
2315
|
-
// `--editions-root` (and its `=` spelling, plus the value that follows
|
|
2316
|
-
// the bare form) is consumed by `editionsRoot()` in lib.mjs, off
|
|
2317
|
-
// `process.argv`, before this parser ever runs. It has to be accepted here or
|
|
2318
|
-
// the flag that decides WHICH editions root is read is rejected by the one
|
|
2319
|
-
// command that reads it.
|
|
2320
|
-
const rest = process.argv.slice(3);
|
|
2321
|
-
const unknownArgs = [];
|
|
2322
|
-
let previousLockPath;
|
|
2323
|
-
let previousLockSha256;
|
|
2324
|
-
let firstSeal = false;
|
|
2325
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
2326
|
-
const argument = rest[i];
|
|
2327
|
-
if (argument === '--trusted-write-v3') {continue;}
|
|
2328
|
-
if (argument === '--first-seal') {
|
|
2329
|
-
firstSeal = true;
|
|
2330
|
-
continue;
|
|
2331
|
-
}
|
|
2332
|
-
if (argument === '--previous-lock' || argument === '--previous-lock-sha256') {
|
|
2333
|
-
i += 1;
|
|
2334
|
-
if (rest[i] === undefined) {throw new Error(`${argument} needs a value`);}
|
|
2335
|
-
if (argument === '--previous-lock') {previousLockPath = rest[i];} else {previousLockSha256 = rest[i];}
|
|
2336
|
-
continue;
|
|
2337
|
-
}
|
|
2338
|
-
unknownArgs.push(argument);
|
|
2339
|
-
}
|
|
2340
|
-
if (unknownArgs.length > 0) {
|
|
2341
|
-
throw new Error(`unknown argument "${unknownArgs[0]}"`);
|
|
2342
|
-
}
|
|
2343
|
-
await writeResolvedImages(id, { trustedWriteV3, previousLockPath, previousLockSha256, firstSeal });
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
if (isEntryModule(import.meta.url)) {
|
|
2347
|
-
main().catch((error) => {
|
|
2348
|
-
console.error(`[dist:images] ${error.message}`);
|
|
2349
|
-
process.exit(1);
|
|
2350
|
-
});
|
|
2351
|
-
}
|