@xemahq/xema 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -389
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +8715 -807
- package/dist/edition/verify-image-evidence.bundle.provenance.json +873 -405
- package/edition/README.md +53 -273
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/authorization-base.mjs +18 -9
- package/edition/build-runtime-image-source-index.mjs +77 -4
- package/edition/catalogue.mjs +15 -12
- package/edition/customer-definition.mjs +3 -20
- package/edition/develop-customer-edition.mjs +2 -2
- package/edition/develop-edition-resolve.mjs +10 -2
- package/edition/develop-edition.mjs +56 -62
- package/edition/develop-freeze.mjs +14 -3
- package/edition/dist-check.mjs +1 -0
- package/edition/distribution-evidence-set.mjs +32 -5
- package/edition/edition-channel.mjs +15 -1
- package/edition/edition-locks.mjs +16 -6
- package/edition/edition-protocol.mjs +30 -8
- package/edition/edition-workspace.mjs +8 -0
- package/edition/image-evidence.mjs +133 -25
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +73 -1
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/oras.mjs +32 -10
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- package/edition/produce-distribution-completeness-evidence.mjs +5 -0
- package/edition/produce-image-delivery-entitlement-evidence.mjs +11 -1
- package/edition/release-catalogue.mjs +10 -3
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +29 -33
- package/edition/resolve-distribution.mjs +5 -1
- package/edition/resolve-edition.mjs +3 -2
- package/edition/seal-image-digests.mjs +16 -4
- package/edition/trust-policy-arming-verb.mjs +2 -10
- package/edition/verify-image-evidence.mjs +1 -0
- package/examples/README.md +3 -2
- package/package.json +3 -3
package/edition/README.md
CHANGED
|
@@ -16,7 +16,7 @@ no defaults.
|
|
|
16
16
|
| `--data-root` | catalogue indices (`biome-index.json`, `platform-service-index.json`, `runtime-image-source-index.json`), `chart-set.lock.json`, and `edition-workspace.json` |
|
|
17
17
|
| `--editions-root` | the `<edition-id>/` definitions and locks |
|
|
18
18
|
| `--sources-root` | source checkouts at `repos/<r>` and `submodules/<r>` |
|
|
19
|
-
| `--kernel` | the BUILT `kernel-contracts` package directory |
|
|
19
|
+
| `--kernel` | the BUILT `kernel-contracts` package directory (vendor verbs only; a customer verb takes none) |
|
|
20
20
|
| `--deploy-root` | the `xema-deploy` checkout |
|
|
21
21
|
| `--cache-dir` | a writable directory for fetched artifacts |
|
|
22
22
|
|
|
@@ -28,7 +28,7 @@ Each takes the roots below plus its own arguments (`--check`, an edition id, ...
|
|
|
28
28
|
| --- | --- |
|
|
29
29
|
| `resolve-distribution.mjs` | data, editions, sources, kernel |
|
|
30
30
|
| `resolve-images.mjs` | data, editions, sources, kernel, cache |
|
|
31
|
-
| `resolve-chart-set.mjs` | data, editions,
|
|
31
|
+
| `resolve-chart-set.mjs` | data, editions, kernel |
|
|
32
32
|
| `edition-biome-manifests.mjs` | data, editions, sources, kernel |
|
|
33
33
|
| `seal-image-digests.mjs` | data, editions, kernel |
|
|
34
34
|
| `verify-image-platforms.mjs` | data, editions, kernel |
|
|
@@ -59,292 +59,72 @@ Read strictly: every key is required, an unknown key or a duplicate key is refus
|
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
- `imageRegistry`: `<host>/<namespace
|
|
63
|
-
- `chartRegistry
|
|
64
|
-
- `
|
|
65
|
-
- `
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
- `imageRegistry`: `<host>/<namespace>` where first-party images are addressed.
|
|
63
|
+
- `chartRegistry`, `chartPublishers`: where chart-set indexes and charts are published. Vendor only; declare both or neither.
|
|
64
|
+
- `sourceRepositoryUrlPrefix`, `sourceAuthUsername`: the prefix every first-party source URL must match and the git username source tokens authenticate as. The token is read from `EVIDENCE_REPOSITORIES_TOKEN` and sent only to repositories under the prefix.
|
|
65
|
+
- `evidenceTrustPolicy`: `{policy, pin}` (data-root-relative paths) or `null` for a data root that declares no policy. A `null` data root serves develop editions only.
|
|
66
|
+
|
|
67
|
+
## Common commands
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
xema edition resolve <id> [--check] # write or verify the lock set
|
|
71
|
+
xema edition seal <id> # seal image digests
|
|
72
|
+
xema edition check # committed locks and indices are current
|
|
73
|
+
xema edition why <id> <biome> # why a biome is in an edition
|
|
74
|
+
xema edition list --editions-root <dir>
|
|
75
|
+
xema edition locks <id> --channel <release|develop> # validated locks as JSON; an unsealed distribution lock is read on develop only
|
|
76
|
+
xema edition artifact <id> --channel release --registry <ns> --coordinate <tag> --engine-image <repo>@sha256:<digest> --out <dir>
|
|
77
|
+
xema edition develop <freeze-sources|resolve|target|check-existing> ...
|
|
78
|
+
|
|
79
|
+
`check-overrides --overrides <json> --data-root <dir>` takes the data root only (the pin roles it judges against) and refuses every other root flag.
|
|
80
|
+
xema edition chart-set <resolve|source-commit> ...
|
|
81
|
+
xema edition arming --channel <release|develop> --data-root <dir> --kernel <dir>
|
|
82
|
+
xema edition verify-evidence <id> --descriptor <abs path> [--json]
|
|
83
|
+
xema edition evidence <assemble-input|check-committed|plan-refresh|statement|assemble-set|oci> ...
|
|
84
|
+
```
|
|
70
85
|
|
|
71
|
-
|
|
72
|
-
- **`xema edition resolve <id> --trusted-write-v3`** writes the lock set, and the flag is the operator's explicit acknowledgement that the images lock is rewritten unsealed (`xema edition seal` writes the digests afterwards). **`xema edition resolve <id> --check`** writes nothing committed (it resolves into a scratch copy) and compares like with like: the resolve timestamp is ignored and the committed images lock is read without its sealed `imageDigest`s, so a sealed lock is not drift.
|
|
73
|
-
- **Git credentials.** `git-source-auth.mjs` is the one place a token becomes git authentication (Basic `<username>:<token>`, `GIT_CONFIG_*`, never argv; the username is a required declared argument with no default). Only the data root declares source credentials: `sourceRepositoryUrlPrefix` and `sourceAuthUsername` in `edition-workspace.json`, with the token in `EVIDENCE_REPOSITORIES_TOKEN`. `holdSource` and `withSourceGitEnv` are the hold and the git environment every network git call on a source passes (the develop freeze's `ls-remote`, the source walk, the indexed checkout), before anything is spawned: a source carrying `authTokenEnv` or `authUsername` is refused, a URL that is not a canonical public https URL is refused, a source under the prefix gets the header scoped to the prefix's origin (an unset token is refused; there is no ambient-credential fallback), and any other source is fetched with no credential: environment-injected git config (`GIT_CONFIG_*`) and askpass programs are removed and the credential helper list is reset. Git runs isolated from the user's own configuration (`GIT_CONFIG_GLOBAL=/dev/null`, `GIT_CONFIG_NOSYSTEM=1`, `HOME` and `XDG_CONFIG_HOME` an empty directory that exists only for the call) and does not follow redirects for a credentialed fetch. The CLI's runtime has its own TypeScript implementation (`src/runtime/sources/kinds/git.ts`) of the same shape, because this library is plain ESM that cannot import compiled TypeScript.
|
|
74
|
-
- **Edition artifacts (one layout for develop and release).** An edition is an OCI artifact at `<registry root>/<channel segment>/xema-edition-<id>` (release: `<root>/editions/xema-edition-<id>`, develop: `<root>/develop-editions/xema-edition-<id>`; both segments and the prefix come from `edition-contract.json` `repositories`), addressed by DIGEST, with artifactType `application/vnd.xema.edition.v1+json` and titled layers (`org.opencontainers.image.title` = file name): `xema-edition.json` (the descriptor) plus `distribution.lock.json`, `images.lock.json`, `authorization.lock.json` and the chart-set lock. `--registry` is the registry ROOT (`<host>[:port]/<namespace>[/...]`, e.g. `registry.example.com/acme`); the CLI appends the channel segment AND `xema-edition-<id>` itself and refuses a root whose last segment is either channel segment or starts with `xema-edition-`. `parseEditionRepository` is the one parser of `<root>/<segment>/xema-edition-<id>`; every reader that fetches an edition or offering artifact by reference refuses a reference whose repository channel is not the channel being resolved, and an artifact whose own stated channel (the catalogue's `channel`) differs from the channel of the repository it was fetched from, before using any content. Tags are for humans only: develop `<snapshot hash>`, release `<coordinate>`. The CLI writes the files to a scratch directory and prints the target; the workflow does `oras push` and `cosign sign` on the digest. Develop: `xema edition develop resolve <id> --pins <file> --out <dir> --registry <root> --engine-image <repo>@sha256:<digest> [--local]`. Release: `xema edition artifact <id> --channel release --registry <root> --coordinate <tag> --engine-image <repo>@sha256:<digest> --out <dir> [--local]`, written from the committed locks and refused when the images lock is not sealed.
|
|
75
|
-
- **The descriptor** (`xema.edition/v1`, strict parse, no other key): `{schema, distributionId, locks[], provenance: {channel: "develop" | "release", pins, workflow: {repository, runId}}, evidenceTrustPolicy: "armed" | "not-armed", engine: {image: "<repository>@sha256:<64 hex>"}}`. `engine.image` is required, digest form only, filled from `--engine-image` (the producer workflow resolves the engine tag with oras; the CLI never resolves tags). `evidenceTrustPolicy` records `armed` when the data root declares a policy and it is armed, `not-armed` otherwise. A release edition's `provenance.pins` is empty (its sources are recorded in its locks). Admission rests on the cosign signature of the manifest digest: readers never branch on `provenance.channel`, and no tool refuses an edition for a channel label, schema name or file name.
|
|
76
|
-
- **Contract.** `edition-contract.json` (`xema.edition-contract/v2`) holds the artifactType, the repository prefix, `repositories` (the namespace segment of each channel: `release` and `develop`), the descriptor file and schema, the lock list, the develop image namespace segment and a `vendoring` statement; it ships in the package. A consumer (xema-deploy, nexus) vendors it BYTE-IDENTICAL from a stated xema-cli commit, never edits it, reads both namespace segments and the prefix only from it, and refuses a contract without `repositories`.
|
|
77
|
-
- **Chart set.** ONE `chart-set.lock.json` (rostered, `schemaVersion: 4`: `publishers[]` with each publisher's `indexDigest`, the registry manifest digest of its index chart, and each chart's version and registry manifest digest; `indexDigest` is `null` exactly for a develop snapshot version `0.0.0-dev.<sha12>`, which has no index chart, and a release lock refuses `null`; a lock of another schema is refused by every reader: re-resolve the chart set with `xema edition chart-set resolve`) serves both channels. A develop edition pins the charts xema-deploy publishes as OCI for the frozen deploy SHA, at version `0.0.0-dev.<first 12 hex of the SHA>` (also the OCI tag). Each producer validates the chart coordinates of its own channel: a release chart set accepts only `-h<16 hex>` versions, a develop one only `-dev.<12 hex>`; a develop chart-set lock copied into a release is refused by `assertReleaseContent`, and `0.0.0-dev.*` index tags are never release candidates; the resolver reads each chart's manifest digest from the chart registry, exactly as a release does, and the lock lists only the publisher whose repository is the deploy pin role's. The CLI computes no chart content digest.
|
|
78
|
-
- **Release lock set.** `assertReleaseEditionTree` checks that the companion chart-set lock is PRESENT beside the distribution and authorization locks. It does not bind the set by a digest, and is not described as hash-bound.
|
|
86
|
+
Results are JSON on stdout and diagnostics on stderr. Exit 0 is success and 1 is a refusal. `xema edition <verb> --help` names the roots and flags each verb takes.
|
|
79
87
|
|
|
80
88
|
## Evidence trust
|
|
81
89
|
|
|
82
|
-
The
|
|
90
|
+
The edition library builds, validates and verifies evidence statements; it never signs. Signing is done by the caller with `cosign`.
|
|
91
|
+
|
|
92
|
+
- An edition descriptor records `evidenceTrustPolicy` as `armed` or `not-armed`. A release edition requires `armed`; writing one otherwise is refused.
|
|
93
|
+
- A policy is armed on a channel when it authorizes a signer for each predicate on that channel and pins `cosign` and `git`. The develop channel admits `not-armed`.
|
|
94
|
+
- The pin file (`image-evidence-trust-policy.pin.json`) binds the policy, the built verifier bundle, the kernel contract authority and the trusted `oras` binaries. Write it with `xema edition repin-evidence-trust [--trusted-oras-digest sha256:<64 hex>]`.
|
|
95
|
+
- `oras` is located with `XEMA_ORAS_BIN` or `PATH` and must hash to a pinned digest. It runs with a scrubbed environment, so proxy and CA variables do not reach it.
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
- `not-armed`: a develop edition only, whose data root declares no policy or one that is not armed on the develop channel. A release edition never records `not-armed`.
|
|
97
|
+
## Kernel protocol and pins
|
|
86
98
|
|
|
87
|
-
|
|
99
|
+
The CLI is coupled to the kernel's edition protocol, not to the kernel package. `@xemahq/contracts` exports the protocol identity as data (`dist/edition-protocol/edition-protocol.json`: `{contentDigest, provisional, version}`). This CLI supports exactly protocol v3 and REFUSES an identity with `provisional: true`; every loader that executes the kernel, `xema edition check` first, and the evidence verifier compare only that identity and refuse an unsupported one, naming the supported and the declared versions. A protocol change needs a CLI release first; only then does the data root repin its kernel authority.
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
- `xema edition evidence statement entitlement stage` writes entitlement predicate v2 (`lockDigest`, `catalogueDigest`, read from the edition's committed locks), so it takes `--editions-root` besides `--data-root` and `--kernel`.
|
|
102
|
+
- The runtime image source index is v2 and carries operator-stated `producerWorkflowPins` (the workflow commit a producer certificate must be signed from, per repository). `xema edition index runtime-image-source [--producer-workflow-pin <owner/repo>=<40-hex>]...` preserves the committed pins exactly; each flag sets one repository's pin (repeatable; a repeated or non-GitHub `owner/repo` is refused; only `--trusted-write` writes an index, so the flag with any other mode, `--check-sources` included, is refused); a bootstrap index with none writes `[]`. A v1 index is not read: re-index the data root.
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
undeclared policy (a top-level `*.json` the kernel parses as an offline trust policy, or a
|
|
93
|
-
`trust/` directory). With one declared it parses the policy with the kernel schema, checks the
|
|
94
|
-
Sigstore trusted root the policy itself names (`trustedRootPath`, resolved against the data root,
|
|
95
|
-
and `trustedRootDigest`), checks the pin file, and classifies the authority configuration of each channel: no
|
|
96
|
-
signer on the channel is UNARMED (on the release channel it fails when the tree holds a release edition and otherwise passes with the notice; on the develop channel it passes with the not-armed line), mixed is PARTIAL (refused on either channel, with
|
|
97
|
-
what is unmet), complete is ARMED.
|
|
104
|
+
## Customer editions
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
A customer edition is resolved against a catalogue pinned in `catalogue.pin.json` beside the definition file. The catalogue is the ceiling: it supplies availability, pins and digests and never adds a component. It also names the kernel: `kernel` is `{name, version, integrity}` (`@xemahq/contracts`, an exact `x.y.z`, the npm `sha512-` integrity of that tarball), and a catalogue without it is refused.
|
|
100
107
|
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"trustedOrasBinaryDigests": ["sha256:<bytes of an oras binary>"] }
|
|
108
|
+
```bash
|
|
109
|
+
xema edition resolve --definition <file> --channel <release|develop> \
|
|
110
|
+
--data-root <dir> --sources-root <dir> --cache-dir <dir> --out <dir> \
|
|
111
|
+
--source-image <source>=<registry>/<prefix> [--catalogue-username <u> --catalogue-token-env <VAR>]
|
|
112
|
+
xema edition seal <id> --definition <file> --data-root <dir> --editions-root <dir> --sources-root <dir> --cache-dir <dir>
|
|
107
113
|
```
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
declared policy, THIS CLI's built verifier and the kernel contract authority the data root's
|
|
111
|
-
`runtime-image-source-index.json` states. A drifted policy or a verifier that is not the pinned
|
|
112
|
-
one is refused by the check, and a missing built bundle is refused, never skipped.
|
|
113
|
-
|
|
114
|
-
- **Kernel contract authority.** Evidence is held to the authority the PIN records. The index
|
|
115
|
-
states it; an index that states another authority is refused
|
|
116
|
-
(`KERNEL_CONTRACT_AUTHORITY_NOT_PINNED_ONE`), and a data root that declares no evidence trust
|
|
117
|
-
has no authority to hold evidence to (`KERNEL_CONTRACT_AUTHORITY_UNPINNED`): no evidence verb
|
|
118
|
-
runs there. The verifier checks the same pin before it reads a statement.
|
|
119
|
-
- **oras is a trusted executable.** On a data root that declares evidence trust, the `oras`
|
|
120
|
-
binary (`XEMA_ORAS_BIN`, else the first on `PATH`, symbolic links resolved) must hash to one of
|
|
121
|
-
`trustedOrasBinaryDigests`; a private copy of exactly those bytes is what runs, re-checked on
|
|
122
|
-
every use. Without the flag a re-pin keeps the reviewed list; a first pin requires it. A data
|
|
123
|
-
root that declares no evidence trust has no pin: there oras is held to its version only, and
|
|
124
|
-
the run says so. oras always runs under a scrubbed environment (`HOME` an empty temporary
|
|
125
|
-
directory, `PATH=/usr/bin:/bin`, `LANG`, `LC_ALL`, `TZ`).
|
|
126
|
-
- **Known limits of the registry reader.** (1) A registry whose Bearer token realm is on another
|
|
127
|
-
host than the registry itself is refused with the coded error
|
|
128
|
-
`REGISTRY_TOKEN_REALM_CROSS_HOST` (`RegistryTokenRealmError`), so the pull credential is never
|
|
129
|
-
sent to a third host. Customer registries that delegate tokens to a separate auth host (for
|
|
130
|
-
example Docker Hub, or a Harbor or GitLab registry with an external token service) cannot be
|
|
131
|
-
read; there is no setting that allows it. (2) Because the environment is scrubbed, a proxy or
|
|
132
|
-
a private CA stated only in environment variables (`HTTPS_PROXY`, `SSL_CERT_FILE`) does not
|
|
133
|
-
reach oras.
|
|
134
|
-
- **Registry status.** A registry status (404, 401, 403, …) is read only from oras's own error
|
|
135
|
-
line; any other failure of the run is undecided and is thrown, never read as "absent".
|
|
136
|
-
|
|
137
|
-
### Why the verifier can be trusted (the binding)
|
|
138
|
-
|
|
139
|
-
The code under review must be the code that runs. The verifier is ONE file, the bundle
|
|
140
|
-
`dist/edition/verify-image-evidence.bundle.mjs`, built at CLI build time (`scripts/build-evidence-verifier.mjs`,
|
|
141
|
-
exact-pinned esbuild) from the CLI's own sources and its locked `@xemahq/contracts` and `zod`.
|
|
142
|
-
It ships in the npm tarball, which is immutable and integrity-addressed, so anyone can recompute
|
|
143
|
-
its digest from `@xemahq/xema@<version>`; `...bundle.provenance.json` lists every bundled input as
|
|
144
|
-
`{package, version, path in that package, sha256}` and is deterministic (building twice gives
|
|
145
|
-
identical bytes). The reviewed data root PINS the bundle's and the provenance's digests, so
|
|
146
|
-
changing the verifier needs a reviewed data-root commit. At run (`xema edition verify-evidence`,
|
|
147
|
-
which launches the built bundle with a reduced environment) the bundle refuses unless:
|
|
148
|
-
|
|
149
|
-
1. its own file (realpath of `argv[1]`) and its provenance hash to the data-root pin;
|
|
150
|
-
2. its install path and every ancestor are not group/world writable nor foreign-owned;
|
|
151
|
-
3. the environment carries no `NODE_OPTIONS`, `NODE_PATH`, `DYLD_*`, `LD_*`, `GIT_*`;
|
|
152
|
-
4. the `--kernel` it is given (the built kernel `xema edition check` proves against the authority
|
|
153
|
-
in `runtime-image-source-index.json`) declares exactly the edition protocol compiled into the
|
|
154
|
-
bundle, and this CLI supports that protocol (`edition-protocol.mjs`); nothing else about the
|
|
155
|
-
kernel is compared;
|
|
156
|
-
5. the data root and the editions root are clean git checkouts at an exact HEAD, every file read
|
|
157
|
-
from them bound to its HEAD blob, graded with a digest-pinned git binary (the completeness
|
|
158
|
-
signature is bound to the editions checkout's HEAD).
|
|
159
|
-
|
|
160
|
-
Not here, vendor-side: `require-verified-image-evidence.mjs`, `deliver-images.mjs`,
|
|
161
|
-
`delta-build-plan.mjs`, the evidence producers, and `edition-pin.mjs` (publish plane).
|
|
162
|
-
|
|
163
|
-
### Product-data sections
|
|
164
|
-
|
|
165
|
-
The library owns the protocol (the edition schema, the resolution algorithm, the closed
|
|
166
|
-
`installationClass` vocabulary, lock and seal rules, the digest format, the trust-policy
|
|
167
|
-
schema) and never product data. Every product fact below is the data root's, in the same
|
|
168
|
-
file, strictly parsed (unknown, missing or mistyped keys are refused, naming the section).
|
|
169
|
-
A section may be absent where a data root legitimately has none; the operation that needs
|
|
170
|
-
it refuses by name when it runs, and never skips.
|
|
171
|
-
|
|
172
|
-
| Section | Holds | Needed by |
|
|
173
|
-
| --- | --- | --- |
|
|
174
|
-
| `firstPartySources` | `{ "<repos/\|submodules/ handle>": "<declared source id>" }` | naming a composed-process subject after its source |
|
|
175
|
-
| `platformServiceSources` | per platform service: `name`, `tier`, `installationClass?`, `repo`, `manifestPath`, `manifestName`, `imageKey`, `runtimeBinding` (`null` or `{subjectKind, subjectId}`) | the platform service index (derived from these rows; no edition is read), image subjects, the runtime image source index |
|
|
176
|
-
| `connectorSources` | `repository`, `bindings[{id, path}]`, `nonProductionIds`, `buildInput{consumerRepo, consumerService, destinationRoot}` | biome discovery and the connector cross-repo inputs; absent means no connector repository |
|
|
177
|
-
| `runtimeImageSources` | `supplementalSources` (the host image, standing services, runtime components), `crossRepoBuildInputs`, `buildStepContextDirectories`; the `host`-kind source's repo is the one repo web biomes are delivered from | the runtime image source index, web-biome deliverability |
|
|
178
|
-
| `pinRoles` | the snapshot pin key of the `data`, `kernel` and `deploy` roles | develop snapshots |
|
|
179
|
-
| `confinedBiomes` | `[{biomeId, holds, consequence, admittedEditions}]` | the confinement check; absent means nothing is confined |
|
|
180
|
-
|
|
181
|
-
`schemaVersion` stays 1: the sections are additive, and a file written for this library is
|
|
182
|
-
refused by a library that predates them (unknown key), never mis-read.
|
|
183
|
-
|
|
184
|
-
The `develop` freeze flags name roles, not repositories: `--data-sha`, `--kernel-sha`,
|
|
185
|
-
`--deploy-sha` (they were `--distributions-sha`, `--kernel-contracts-sha`,
|
|
186
|
-
`--xema-deploy-sha`; no aliases).
|
|
187
|
-
|
|
188
|
-
## Evidence verbs (consumed by vendor-ops)
|
|
189
|
-
|
|
190
|
-
The edition library builds, validates and verifies evidence statements. It NEVER signs: it holds no key, requests no OIDC token, and its cosign runner refuses every subcommand except `verify-blob-attestation`. A caller (vendor-ops) runs `cosign attest-blob`, `oras`, `git` and writes its own step outputs; it reads the JSON these verbs print and never parses a lock, a statement or a policy itself.
|
|
191
|
-
|
|
192
|
-
Common to all verbs: root flags `--data-root`, `--editions-root`, `--sources-root`, `--kernel` (each verb names the ones it needs); JSON results go to stdout, diagnostics to stderr; exit 0 on success, 1 on any refusal (plus 2 for `plan-refresh` when a refresh is due). The kernel contract authority every statement binds is the one the data root's evidence-trust pin records (see Evidence trust); the trust policy is the one `edition-workspace.json` declares, held to its pin.
|
|
193
|
-
|
|
194
|
-
| Verb | Arguments | Result |
|
|
195
|
-
|---|---|---|
|
|
196
|
-
| `xema edition chart-set resolve [--check]` | roots `--data-root --editions-root --deploy-root --kernel` | what `node resolve-chart-set.mjs` did: pins `chart-set.lock.json` and every edition's copy to the charts the registry serves; `--check` writes nothing and fails when the pin is stale. The index chart is pinned by the manifest digest the registry served at resolve, and every later read is by that digest; WHO signed the index is not verified by the CLI (the control is the reviewed lock commit) |
|
|
197
|
-
| `xema edition chart-set source-commit` | roots `--data-root --kernel`, env the registry credential variables the data root declares | read-only: for each publisher of the committed `chart-set.lock.json` it fetches the index chart by the LOCKED `indexDigest` (a develop publisher, `indexDigest: null`, is refused) and prints `{"schema":"xema.edition-chart-set-source/v1","publishers":[{"index","repo","version","sourceCommit"}]}` (the `xema.dev/chart-set-source-sha` annotation). It prints nothing and exits non-zero when the registry serves another digest, when the annotation is absent or not 40 lowercase hex, or when the index's roster differs from the charts the lock records for that publisher |
|
|
198
|
-
| `xema edition biome-manifests <id> --json` | `--data-root --editions-root --sources-root --kernel` | `{"schema":"xema.edition-biome-manifests/v1","distributionId","biomes":[{"id","repo","sourceCommit","manifest"}]}` sorted by id: each biome's `xema-biome.json` read at the commit its lock pins; an absent lock or an unreadable manifest is refused with nothing on stdout |
|
|
199
|
-
| `xema edition arming --channel <release\|develop>` | `--data-root --kernel` | read-only: `{"channel","state":"armed\|unarmed\|partial","unmet":[…]}` on one stdout line; exits 0 only when the declared evidence trust policy is `armed` on that channel, 1 otherwise (each unmet entry on stderr; a data root declaring no policy is `unarmed`), 2 for a usage error |
|
|
200
|
-
| `xema edition list` | `--editions-root` | `{"schemaVersion":1,"editions":["<id>",…]}`; an empty root is refused |
|
|
201
|
-
| `xema edition locks <id>` | `--data-root --editions-root --kernel` | `{"schemaVersion":1,"distributionId","contractAuthority","distributionLock","imageLock","repositoryPins":[…]}`, validated as the verifier validates them |
|
|
202
|
-
| `xema edition evidence assemble-input` | `--output <file> --image-repository --manifest-digest --subject-kind --subject-id --source-repo --source-path --source-commit --provenance <file> [--source-manifest-hash] [--source-tree-hash] [--ttl-seconds]`, roots `--data-root --sources-root --kernel`, env `GITHUB_*` workflow context and the registry credential variables the data root declares | writes the producer-input JSON (mode 0600, never overwrites); reads the registry with the pinned oras |
|
|
203
|
-
| `xema edition evidence check-committed` | `--input <file> --repository-root <dir> [--github-output <file>] [--now <epoch-ms>]`, roots `--data-root --kernel` | prints `REUSE`/`PRODUCE <directory>: <reason>`; with `--github-output` appends `reusable`, `evidence-directory`, `reason` |
|
|
204
|
-
| `xema edition evidence plan-refresh` | `--distribution <id>… [--horizon-seconds] [--now] [--github-output <file>] [--kernel-checkout exact\|preview]`, roots `--data-root --editions-root --kernel` | prints the plan; exit 2 when a refresh is due; `--github-output` appends `refresh-due`, `due-subjects`, `total-subjects`, `due-repositories`; producer evidence is read only from the OCI referrers of each locked image. `--kernel-checkout exact` (default) loads the kernel from a clean checkout at the pinned authority commit; `preview` uses the kernel built at `--kernel`, held to no commit: it prints `LOCAL PREVIEW` and refuses `--github-output` |
|
|
205
|
-
| `xema edition evidence statement producer stage` | `--input <file> --staging <abs dir> --github-repository --github-ref --github-sha --github-workflow-ref`, roots `--data-root --kernel` | creates the staging directory with `statements/<stem>.json`; prints the signing plan |
|
|
206
|
-
| `xema edition evidence statement producer accept` | the `stage` flags plus `--output <abs dir> --cosign <abs path>` | verifies each bundle, builds the output from the statements the input derives and the canonical verified bundles ONLY (the staging must hold exactly what `stage` recorded), writes `producer-evidence.json`, and removes the staging whether it succeeds or refuses; the cosign version is the trust policy's `cosignVersion`; prints `{"schemaVersion":1,"kind":"producer","outputDirectory","producerEvidence":[…]}` |
|
|
207
|
-
| `xema edition evidence statement completeness stage` | `--distribution --staging <abs dir> --release-sequence --release-source-commit --run-id --run-attempt --issued-at --expires-at --signer-repository --signer-ref --signer-workflow-path --github-repository --github-ref --github-sha --github-workflow-ref `, roots `--data-root --editions-root --kernel` | stages the edition's producer evidence under `producers/` and `completeness/statement.json`; prints the signing plan |
|
|
208
|
-
| `xema edition evidence assemble-set` | the completeness `stage` flags plus `--evidence-root <abs dir> --cosign <abs path>` | re-resolves the producer evidence (it must equal the staged copy), verifies the signed completeness bundle, writes `evidence-set.json`, builds the evidence root from the derived files ONLY (the staging must hold exactly what `stage` recorded) and removes the staging whether it succeeds or refuses; prints `{"schemaVersion":1,"kind":"evidence-set","distributionId","evidenceRoot","evidenceSetPath","producerBundles","completenessStatementDigest"}` |
|
|
209
|
-
| `xema edition evidence statement entitlement stage` | `--distribution --evidence-root <dir> --staging <abs dir> --customer --mode [--target-registry] --idempotency-key --run-id --run-attempt --issued-at --expires-at --signer-repository --signer-ref --signer-workflow-path --github-repository --github-ref --github-sha --github-workflow-ref`, roots `--data-root --kernel` | writes `entitlement/statement.json`; prints the signing plan |
|
|
210
|
-
| `xema edition evidence statement entitlement accept` | the `stage` flags plus `--output <abs dir> --cosign <abs path>` | verifies the bundle, writes `entitlement.json`, builds the output from the derived files ONLY (the staging must hold exactly what `stage` recorded) and removes the staging whether it succeeds or refuses; prints `{"schemaVersion":1,"kind":"entitlement",…,"evidence":{…}}` |
|
|
211
|
-
| `xema edition evidence oci attach-plan` | `--evidence-directory <abs dir> [--input <file>]`, roots `--data-root --kernel` | validates a produced evidence directory (with `--input`, against the statements that input builds) and prints `{"schemaVersion":1,"kind":"oci-attach-plan","evidenceDirectory","imageRepository","subjectDigest","subject","artifactType","distributionSpec","annotations":{…},"files":[{"path","mediaType"}]}`: the operands of `oras attach`, run from the evidence directory |
|
|
212
|
-
| `xema edition evidence oci validate` | `--evidence-directory <abs dir> --manifest <file> --subject-digest <digest> [--input <file> --cosign <abs path> [--now <epoch-ms>]]`, roots `--data-root --kernel` | holds a pulled artifact (`oras pull`) to its manifest (`oras manifest fetch`); prints `{"schemaVersion":1,"kind":"oci-evidence","subjectDigest","created","subjects":[…],"matchesInput","remainingSeconds","reusable"}`. The last three are `null` without `--input`; with it every bundle is verified against the input's signer and a bundle that does not verify is a refusal |
|
|
213
|
-
| `xema edition evidence oci backfill-plan` | `--distribution <id>… --producer-repository-root <abs dir>…`, roots `--data-root --editions-root --kernel`, env the registry credential variables the data root declares | prints `{"schemaVersion":1,"kind":"oci-backfill-plan","items":[{"status":"missing-git\|present\|missing","subjectKind","subjectId","imageRepository","subjectDigest","artifactDigest","attach"?}]}`; a `missing` item carries its attach plan; exit 2 unless every item is `present` |
|
|
214
|
-
| `xema edition index runtime-image-source --repin-contract-authority --kernel-commit <40-hex>` | roots `--data-root --sources-root --kernel`; `--kernel-commit` REQUIRED (the kernel commit the authority moves to; refused with the usage when absent); `[--kernel-root <absolute-checkout>]` | replaces ONLY `contractAuthority` of the committed `runtime-image-source-index.json` with the authority read from the kernel checkout AT THE STATED COMMIT: the checkout must be clean, at the expected origin, and its HEAD must equal `--kernel-commit` (else refused, naming both commits), and that commit is what `contractAuthority.sourceCommit` records and what `xema edition repin-evidence-trust` then pins as `kernelContractAuthority.sourceCommit`. A bootstrap `--trusted-write` (no committed contract authority) chooses a commit the same way and takes the same required flag; every other mode refuses it. It validates and rewrites the index; every repository pin and subject is identical before and after. It writes the move on stdout and the owed follow-ups on stderr: `xema edition repin-evidence-trust`, re-resolving every edition (each `images.lock.json` carries the authority), and that signed producer statements made under the old authority no longer verify. `--kernel-commit` holds the checkout's HEAD to the stated commit and nothing more: there is NO ancestry or remote check, so a never-pushed local commit passes if its SHA is stated; the control is the reviewed data-root diff that carries the new `sourceCommit` |
|
|
215
|
-
| `xema edition source-closure` | `--repo <repo> --path <dir> --commit <40 hex>`, roots `--sources-root --kernel` (the hash implementation is held to the kernel) | `{"schemaVersion":1,"repo","directory","commit","treeAlgorithm","treeHash","buildable","lockfileBlobId","closure":[{"directory","treeHash"}]}`: the facts a content coordinate is framed from |
|
|
216
|
-
| `xema edition verify-evidence <id>` | see Evidence trust; `--descriptor <absolute path of the edition descriptor being verified>` (required: its `distributionId` must be the requested id and its `provenance.channel` is the channel whose signers may vouch for the evidence; it is held outside the graded checkouts like `--state`); plus `[--json]` (stdout is then exactly one `{"schema":"xema.edition-evidence-verification/v1","distributionId","channel","releaseSequence","statements":[{"predicateType","subject","statementDigest"}]}` document, where `statementDigest` is the digest of the verified statement, not of the image; the human text goes to stderr, and a failed verification prints no JSON); plus, given together or not at all, `--entitlement <evidence-root-relative path of an entitlement.json> --entitlement-workflow-sha <40-hex git sha> --customer <id> --mode <mode>` and optionally `--target-registry <registry>`: the delivery entitlement `accept` wrote is verified with the release (`--entitlement-workflow-sha` is the exact commit of the workflow run that signed the entitlement, the one its certificate is held to; it is stated by the caller and never read from the evidence; the statement and bundle are read from the directory holding `entitlement.json`), and the verification document then ends with the entitlement statement | the in-repository gate and the release-checkout grading are this verifier; there is no second one. Its result is its exit status, the advanced replay state and, with `--json`, the verification document on stdout: it writes no "already verified" file and has no file-output flag. A delivery entitlement verifies only as part of this run: a refused entitlement refuses the whole verification and the replay state does not advance. `entitlement.json` is `{schemaVersion: 2, distributionId, customerId, mode, targetRegistry, idempotencyKey, distributionStatementDigest, evidence}` with exactly those keys (`evidence` is the Sigstore evidence descriptor); every field but `evidence` is an unauthenticated convenience that must agree with the flags and the verified completeness statement, and the signed statement stays the authority |
|
|
217
|
-
|
|
218
|
-
The signing plan (stdout of every `stage`):
|
|
115
|
+
To use a customer's own registry, copy the catalogue artifact and vendor images by digest with `oras cp`, pin the mirrored catalogue in `catalogue.pin.json`, and declare `imageRegistryMirror: {complete: true, map: [{source, target}]}` in the definition.
|
|
219
116
|
|
|
220
|
-
|
|
221
|
-
{"schemaVersion":1,"kind":"producer|completeness|entitlement","root":"<staging dir>","predicateType":"<uri>",
|
|
222
|
-
"signer":{"certificateIdentity":"…","oidcIssuer":"…","githubWorkflow":{"repository":"…","ref":"…","path":"…"}},
|
|
223
|
-
"certificateWorkflowSha":"<40 hex>","statements":[{"statementPath":"<relative>","bundlePath":"<relative>"}]}
|
|
224
|
-
```
|
|
117
|
+
A customer verb takes no `--kernel` (it is refused) and needs no kernel checkout and no credential for the kernel. After it fetches its catalogue it downloads `@xemahq/contracts@<catalogue.kernel.version>` from registry.npmjs.org into `<--cache-dir>/kernel-npm/`, holds the tarball to the catalogue's npm integrity before extracting it, and proves the package is the catalogue's contract authority (final protocol identity carrying `contractAuthority.contentDigest`) before it loads any schema.
|
|
225
118
|
|
|
226
|
-
For each entry the caller runs `cosign attest-blob --yes --statement <root>/<statementPath> --bundle <root>/<bundlePath> --new-bundle-format <root>/<statementPath>` and then the matching `accept` (or `assemble-set`) with the SAME flags it staged with. `accept` re-derives every statement from those flags and refuses staged bytes that differ, so nothing but what the library built can be accepted; it rewrites the bundle as canonical JSON, requires its payload to be exactly the statement bytes, and runs `cosign verify-blob-attestation` against the plan's signer at `certificateWorkflowSha`. The staging directory must not exist before `stage`; the output is built in a private sibling `<output>.accepting-<pid>` on the output's filesystem and renamed into place. `accept` removes the staging whether it succeeds or refuses (a refusal of its arguments before the first read leaves it), so a signed-but-unaccepted bundle is never left behind; stage and sign again after a refusal.
|
|
227
119
|
|
|
228
|
-
|
|
120
|
+
### Adapter bundles against the adapter host
|
|
229
121
|
|
|
230
|
-
`
|
|
231
|
-
Tests build throwaway roots with `test-fixtures/workspace.mjs`. The kernel is the
|
|
232
|
-
CLI's own installed `@xemahq/contracts`; nothing is supplied from outside and no test
|
|
233
|
-
skips. The two sealing cases run the real openfga/cli v0.8.1 and FAIL, naming
|
|
234
|
-
`XEMA_FGA_CLI`, when it is unset (CI installs it by checksum). `no-vendor-literals.test.mjs` fails on any organisation or
|
|
235
|
-
registry literal in a shipped file.
|
|
122
|
+
The adapter host refuses at mount an adapter whose packed `package.json` `xemaBundled` entry for `@xemahq/contracts` or `@xemahq/biome-sdk` is missing or not caret-compatible with the host's own resolved copy (same major; same minor too while the major is 0; an unparsable version is incompatible). The edition build refuses the same first, in `adapter-bundled-versions.mjs`: every lock biome host module of the `xema-adapter-host` ABI must carry an npm bundle source, whose published manifest is read from `https://registry.npmjs.org/<name>/<version>` without a credential. The host's versions are the `hostPackageVersions` of the `biome-adapter-host` row of the biome index (read from the host source's `pnpm-lock.yaml` when the index is built; the distribution lock never carries it) and, for a customer, of the same row in the catalogue. Resolve (vendor, develop, customer) and seal (vendor and `--definition`) run the check and report all refusals together, each with one of `adapter-bundle-version-mismatch`, `adapter-host-unpinned` (no host row or no `hostPackageVersions`), `adapter-not-published` (the registry answers 404) or `unsupported-bundle-source` (not npm).
|
|
236
123
|
|
|
237
|
-
##
|
|
124
|
+
## Tests
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm run test:edition # node --test "edition/*.test.mjs" "edition/*-contract-tests.mjs"
|
|
128
|
+
```
|
|
238
129
|
|
|
239
|
-
|
|
240
|
-
(`xema.edition-catalogue/v1`: `offeringId`, `channel` (provenance only), `biomes`, `platformServices`, `images`,
|
|
241
|
-
`chartSet`, `contractAuthority`, `ceiling`). The catalogue is the ceiling: it supplies availability, pins, digests and
|
|
242
|
-
the chart-set, and never adds a component. The resolved set is the customer's `{biome}` selections, every
|
|
243
|
-
`installationClass: core` entry and the transitive declared dependencies.
|
|
244
|
-
|
|
245
|
-
xema edition resolve --definition <file> --channel <release|develop> \
|
|
246
|
-
--data-root <dir> --sources-root <dir> --kernel <dir> --cache-dir <dir> --out <dir> \
|
|
247
|
-
--source-image <source>=<registry>/<prefix> [--catalogue-username <u> --catalogue-token-env <VAR>]
|
|
248
|
-
|
|
249
|
-
The catalogue is the one pinned for the channel in `catalogue.pin.json`, in the SAME directory as the definition file:
|
|
250
|
-
`{"schemaVersion": 1, "develop": "oci://<repository>@sha256:<64 hex>" | null, "release": "oci://<repository>@sha256:<64 hex>" | null}`
|
|
251
|
-
(exact keys, both channel keys present, null = not produced on that channel; the repository of the reference under a key must be
|
|
252
|
-
that channel's repository). A null pin for the requested channel is refused. There is no `--catalogue` flag; passing one is an unknown-flag error.
|
|
253
|
-
`catalogue.pin.json` is written by the customer (or for it, by a commit to its repository), and until the kernel repin the
|
|
254
|
-
catalogue it names carries no vendor signature and no identity check: the digest in the pin is the only thing that binds it.
|
|
255
|
-
`develop freeze-sources --definition` asks only source URLs that are canonical HTTPS repository URLs under the data root's
|
|
256
|
-
`sourceRepositoryUrlPrefix`; a definition naming any other origin is refused before a network call, so the source token is never sent there.
|
|
257
|
-
A definition source carrying `authTokenEnv` or `authUsername` is refused on every path that fetches it: source credentials are declared only by the data root (`sourceRepositoryUrlPrefix` + `sourceAuthUsername`, token `EVIDENCE_REPOSITORIES_TOKEN`), and `resolve` fetches a source outside the prefix with no credential.
|
|
258
|
-
`--registry` refuses a root with a `.` or `..` path segment.
|
|
259
|
-
|
|
260
|
-
`--out` is the customer's editions root (`<out>/<id>/` receives the four locks). `--data-root` holds the customer's
|
|
261
|
-
`edition-workspace.json`. The definition allows `{biome}` selectors (the same per-biome options a vendor offering's selector takes in the kernel
|
|
262
|
-
schema, `installPolicy` and `mandatory`, and nothing else), `sources[]` (no registry), `trustCeiling` and
|
|
263
|
-
`imageRegistryMirror`; `extends`, `exclude`, tier/origin/class selectors, `platformServices`, `visibility` and a source
|
|
264
|
-
registry are refused.
|
|
265
|
-
|
|
266
|
-
Ownership rules of the definition (the kernel's `parseCustomerEditionDefinition` and `customerTrustCeilingIssues`; `customer-definition.mjs` only calls them):
|
|
267
|
-
`sources[].ref` is required and names a branch or a tag; a commit id (any full 40- or 64-hex git object id) is refused,
|
|
268
|
-
because the seal records the resolved commit in the lock (the develop `--pins` file still supplies commits). `trustCeiling`
|
|
269
|
-
is required when the definition declares `sources[]` and refused when it does not. It must assign every customer-source
|
|
270
|
-
biome (a biome drawn from the definition's own `sources[]`) exactly once: a missing biome, a biome assigned twice and an
|
|
271
|
-
extra biome that is not customer-sourced are refused.
|
|
272
|
-
|
|
273
|
-
The vendor's trust domain is closed to a customer edition, by three refusals the CLI makes:
|
|
274
|
-
- a `trustCeiling` assignment whose domain is the kernel's first-party domain is refused when the definition is parsed, naming the biomes and the domain;
|
|
275
|
-
- a biome drawn from a declared `sources[]` entry (of any edition) whose manifest declares an origin other than third-party (`xema.trustTier`) is refused, naming the source and the biome;
|
|
276
|
-
- the catalogue's `ceiling` bounds the resolve. Its `trustPolicy` (origin allow list), when stated, is the one the resolve applies to every biome of the lock.
|
|
277
|
-
Its `trustCeiling`, when stated, is written onto the vendor's rows of the customer lock, and for a customer-source biome it admits one domain: the domain the catalogue
|
|
278
|
-
assigns that biome when it names it, otherwise any domain the catalogue assigns to nothing. A definition that names another domain is refused, naming the biome and both domains.
|
|
279
|
-
A `null` member of `ceiling` bounds nothing.
|
|
280
|
-
|
|
281
|
-
Handoff to the customer's registry: `oras cp` the catalogue artifact and the vendor images by digest, pin the mirrored
|
|
282
|
-
catalogue in `catalogue.pin.json`, and declare `imageRegistryMirror: {complete: true, map: [{source, target}]}`. Prefixes match whole
|
|
283
|
-
path segments; the digest never changes and no signature is re-verified at the mirrored ref (the customer's seal is the
|
|
284
|
-
trust anchor and deploy is digest-anchored).
|
|
285
|
-
|
|
286
|
-
The chart-set is copied whole from the catalogue: it states which deploy charts are available, not which components are
|
|
287
|
-
selected, so it adds no component. `trustCeiling` has the kernel shape `{assignments: [{domain, biomes}]}`.
|
|
288
|
-
|
|
289
|
-
### Locks, sealing and the catalogue's tree ids
|
|
290
|
-
|
|
291
|
-
`resolve` writes the UNSEALED lock (`unsealedSchema`); `seal` attaches each biome's `biomeContentHash` and stamps
|
|
292
|
-
`schemaVersion` 5, the only grammar. The hash is recomputed by `seal` AND by the verify step from the biome's
|
|
293
|
-
`sourceManifestHash`, its git tree id at the pinned commit and the sorted image subject digests, through the kernel's
|
|
294
|
-
`computeBiomeContentHash`; a mismatch (a tampered hash, 64 arbitrary hex digits, a changed manifest, tree id or image
|
|
295
|
-
digest) is refused, naming the biome. `resolve --check` compares the unsealed projection byte for byte.
|
|
296
|
-
|
|
297
|
-
A customer holds no checkout of the vendor's sources. `catalogue.json` therefore carries `sourceTreeIds` (each vendor
|
|
298
|
-
biome's tree id at its pinned commit, written from the real tree by `edition catalogue` / `edition artifact`), and the
|
|
299
|
-
customer's seal takes that tree id as an INPUT ON THE CATALOGUE'S AUTHORITY: it is not source verification, and the
|
|
300
|
-
refusal text says so. A vendor biome absent from the catalogue is refused, not skipped.
|
|
301
|
-
|
|
302
|
-
xema edition seal <id> [--definition <file> [--catalogue-username <u> --catalogue-token-env <VAR>]] \
|
|
303
|
-
--data-root <dir> --editions-root <dir> --sources-root <dir> --kernel <dir> --cache-dir <dir>
|
|
304
|
-
|
|
305
|
-
Mirror runbook (`imageRegistryMirror`): 1. `oras cp` the catalogue artifact and every vendor image BY DIGEST into the
|
|
306
|
-
customer's registry. 2. Pin the mirrored catalogue digest in `catalogue.pin.json`. 3. Declare
|
|
307
|
-
`imageRegistryMirror: {complete: true, map: [{source, target}]}` (whole path segments; `complete: true` refuses a vendor row no
|
|
308
|
-
entry covers). 4. Resolve and seal: digests are unchanged, so the seal's hashes are unchanged; no signature is
|
|
309
|
-
re-verified at the mirrored ref.
|
|
310
|
-
|
|
311
|
-
### Develop customer edition
|
|
312
|
-
|
|
313
|
-
A customer-owned develop edition is produced with CLI verbs only. Each takes `--definition <file>`; the develop catalogue is the
|
|
314
|
-
`develop` pin of `catalogue.pin.json` beside it.
|
|
315
|
-
|
|
316
|
-
xema edition develop freeze-sources --definition <file> --out <file>
|
|
317
|
-
xema edition develop kernel-commit --definition <file> [--catalogue-username <u> --catalogue-token-env <VAR>] --data-root <dir>
|
|
318
|
-
xema edition develop resolve --definition <file> --pins <file> --out <dir> --registry <root> --engine-image <repository>@sha256:<digest> \
|
|
319
|
-
--data-root <dir> --editions-root <dir> --sources-root <dir> --kernel <dir> --cache-dir <dir> \
|
|
320
|
-
[--source-image <source>=<registry>/<prefix>]... [--catalogue-username <u> --catalogue-token-env <VAR>] [--local]
|
|
321
|
-
xema edition develop target --definition <file> --pins <file> --registry <root>
|
|
322
|
-
xema edition develop check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest> \
|
|
323
|
-
--data-root <dir> --kernel <dir>
|
|
324
|
-
|
|
325
|
-
`freeze-sources` writes (exclusively) the `--pins` file: exactly one `sources/<name>` commit per source the definition declares,
|
|
326
|
-
the head of `refs/heads/develop` of each source repository, resolved once (token from `EVIDENCE_REPOSITORIES_TOKEN`).
|
|
327
|
-
`kernel-commit` fetches the pinned develop catalogue and prints its `contractAuthority.sourceCommit` (one 40-hex line): the commit
|
|
328
|
-
a workflow checks the kernel contracts out at; it needs no `--kernel` and no `--sources-root`. `target` prints the develop target
|
|
329
|
-
(the tag covers the pinned catalogue digest). `check-existing` does for a customer artifact what it does for a vendor one, and
|
|
330
|
-
additionally refuses a descriptor whose `catalogue` provenance pin is not the pinned develop catalogue digest.
|
|
331
|
-
|
|
332
|
-
Every customer DEVELOP verb works on a data root holding only `edition-workspace.json`, and none reads the vendor's indices,
|
|
333
|
-
pins or pin roles. The release seal of a customer edition (`xema edition seal <id> --definition ...`) still reads the vendor's
|
|
334
|
-
`runtime-image-source-index.json` from the data root and is not supported before the kernel repin. It takes `--data-root`,
|
|
335
|
-
`--editions-root`, `--sources-root`, `--kernel` and `--cache-dir`, and refuses any other root flag (for example `--deploy-root`),
|
|
336
|
-
naming the roots it takes. A customer whose sources build images also commits `image-registry-credentials.json` in its editions root
|
|
337
|
-
(the registries its own images are verified against); when its own image rows are sealed and that file is absent, the resolve
|
|
338
|
-
is refused naming the file and the root. A customer with no sources, or whose sources build no image, needs no file in its
|
|
339
|
-
editions root. A customer verb refuses a root flag it does not take, naming the roots it takes (`target` takes none).
|
|
340
|
-
|
|
341
|
-
The id is the definition's (no positional id). `--pins` holds exactly one `sources/<name>` commit per source the
|
|
342
|
-
definition declares (the customer's own repositories; the vendor's pins are the catalogue's). The digest of the catalogue
|
|
343
|
-
artifact is frozen into the descriptor's `provenance.pins` as `catalogue: "sha256:<64 hex>"` (typed per key: every other
|
|
344
|
-
pin is a 40-hex commit), so the snapshot-hash tag covers it. The artifact is the one edition layout (`xema.edition/v1`,
|
|
345
|
-
the descriptor plus the four locks) at `<root>/<develop segment>/xema-edition-<id>`. Vendor image rows arrive sealed from the
|
|
346
|
-
catalogue and are neither re-homed nor re-verified; only the customer's own rows are re-homed to the develop image repositories,
|
|
347
|
-
verified there for their required platforms and sealed with the digest the registry served. The images lock is parsed once, as
|
|
348
|
-
the merged lock (vendor rows plus own rows). On the release channel the own rows stay unsealed in the merged lock. A customer artifact is not an
|
|
349
|
-
offering and carries no `catalogue.json`. `edition-contract.json` lists `catalogue.json` under `offeringLayers` (a layer
|
|
350
|
-
of an offering artifact, not a lock) and the `catalogue` pin under `provenancePins`.
|
|
130
|
+
The sealing tests run the real `openfga` CLI and fail, naming `XEMA_FGA_CLI`, when it is unset.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// adapter-bundled-versions — the edition build refuses what the adapter host
|
|
2
|
+
// would refuse at mount, before it ships.
|
|
3
|
+
//
|
|
4
|
+
// The adapter host (xema-base `biome-adapter-host`) refuses at mount an adapter
|
|
5
|
+
// whose packed package.json `xemaBundled` entry for `@xemahq/contracts` or
|
|
6
|
+
// `@xemahq/biome-sdk` is missing or not caret-compatible with the host's own
|
|
7
|
+
// resolved copy. The mount check stays the last line; this is the first.
|
|
8
|
+
|
|
9
|
+
import { ADAPTER_HOST_BIOME_ID, BUNDLED_PACKAGES, isCaretCompatible } from './adapter-host-versions.mjs';
|
|
10
|
+
import { loadKernelBiomeContracts } from './lib.mjs';
|
|
11
|
+
import { compareStrings } from './string-order.mjs';
|
|
12
|
+
|
|
13
|
+
export const AdapterBundleVersionErrorCode = Object.freeze({
|
|
14
|
+
VersionMismatch: 'adapter-bundle-version-mismatch',
|
|
15
|
+
Unpinned: 'adapter-host-unpinned',
|
|
16
|
+
NotPublished: 'adapter-not-published',
|
|
17
|
+
UnsupportedSource: 'unsupported-bundle-source',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export class AdapterBundleVersionError extends Error {
|
|
21
|
+
constructor({ code, biomeId, packageName, bundled, host }) {
|
|
22
|
+
super(
|
|
23
|
+
`${code}: biome "${biomeId}" package "${packageName}" bundled ${JSON.stringify(bundled)} ` +
|
|
24
|
+
`against host ${JSON.stringify(host)}`,
|
|
25
|
+
);
|
|
26
|
+
this.name = 'AdapterBundleVersionError';
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.biomeId = biomeId;
|
|
29
|
+
this.packageName = packageName;
|
|
30
|
+
this.bundled = bundled;
|
|
31
|
+
this.host = host;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The adapter host's row of a biome index or catalogue (undefined when the edition does not carry it). */
|
|
36
|
+
export function adapterHostRow(rows) {
|
|
37
|
+
return rows.find((row) => row.id === ADAPTER_HOST_BIOME_ID);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Does the lock mount any adapter host module? */
|
|
41
|
+
export function lockMountsAdapters(lock) {
|
|
42
|
+
const adapterAbi = loadKernelBiomeContracts().BiomeComponentHostAbi.AdapterHost;
|
|
43
|
+
return (lock.biomes ?? []).some((biome) => (biome.hostModules ?? []).some((hostModule) => hostModule.hostAbi?.name === adapterAbi));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Fetch a package version's manifest from the public registry (no credential); null when unpublished. */
|
|
47
|
+
export async function fetchRegistryManifest(name, version, fetchImpl = fetch) {
|
|
48
|
+
const response = await fetchImpl(
|
|
49
|
+
`https://registry.npmjs.org/${name.replace('/', '%2F')}/${encodeURIComponent(version)}`,
|
|
50
|
+
{ headers: { accept: 'application/json' } },
|
|
51
|
+
);
|
|
52
|
+
if (response.status === 404) {return null;}
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`npm registry answered ${response.status} for ${name}@${version}`);
|
|
55
|
+
}
|
|
56
|
+
return response.json();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refuse every adapter host module whose packed bundle the host would refuse at
|
|
61
|
+
* mount. `fetchManifest(name, version)` resolves the manifest or null (unpublished).
|
|
62
|
+
* All refusals are collected and thrown together, sorted.
|
|
63
|
+
*/
|
|
64
|
+
export async function assertAdapterBundlesMatchHost({ lock, hostRow, fetchManifest }) {
|
|
65
|
+
const adapterAbi = loadKernelBiomeContracts().BiomeComponentHostAbi.AdapterHost;
|
|
66
|
+
const refusals = [];
|
|
67
|
+
const manifests = new Map();
|
|
68
|
+
const hostVersions = hostRow?.hostPackageVersions;
|
|
69
|
+
for (const biome of lock.biomes ?? []) {
|
|
70
|
+
for (const hostModule of biome.hostModules ?? []) {
|
|
71
|
+
if (hostModule.hostAbi?.name !== adapterAbi) {continue;}
|
|
72
|
+
const base = { biomeId: biome.id, bundled: null, host: null };
|
|
73
|
+
if (hostVersions === undefined) {
|
|
74
|
+
refusals.push({ ...base, code: AdapterBundleVersionErrorCode.Unpinned, packageName: ADAPTER_HOST_BIOME_ID });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const source = hostModule.bundleSource;
|
|
78
|
+
if (source?.kind !== 'npm') {
|
|
79
|
+
refusals.push({ ...base, code: AdapterBundleVersionErrorCode.UnsupportedSource, packageName: String(source?.kind) });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const key = `${source.name}@${source.version}`;
|
|
83
|
+
if (!manifests.has(key)) {
|
|
84
|
+
manifests.set(key, fetchManifest(source.name, source.version));
|
|
85
|
+
}
|
|
86
|
+
const manifest = await manifests.get(key);
|
|
87
|
+
if (manifest === null) {
|
|
88
|
+
refusals.push({ ...base, code: AdapterBundleVersionErrorCode.NotPublished, packageName: key });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const name of BUNDLED_PACKAGES) {
|
|
92
|
+
const bundled = manifest.xemaBundled?.[name] ?? null;
|
|
93
|
+
if (!isCaretCompatible(bundled, hostVersions[name])) {
|
|
94
|
+
refusals.push({ ...base, code: AdapterBundleVersionErrorCode.VersionMismatch, packageName: name, bundled, host: hostVersions[name] });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (refusals.length === 0) {return;}
|
|
100
|
+
const errors = refusals
|
|
101
|
+
.map((fields) => new AdapterBundleVersionError(fields))
|
|
102
|
+
.sort((a, b) => compareStrings(a.message, b.message));
|
|
103
|
+
const thrown = new AdapterBundleVersionError(errors[0]);
|
|
104
|
+
thrown.message = errors.map((error) => error.message).join('\n');
|
|
105
|
+
thrown.refusals = errors;
|
|
106
|
+
throw thrown;
|
|
107
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// adapter-host-versions — what the edition knows of the adapter host's own copies of the packages an adapter
|
|
2
|
+
// bundles: the packages compared, the host's caret rule (copied exactly from xema-base
|
|
3
|
+
// biomes/biome-adapter-host/api/biome-adapter-host/src/loader/caret-compatibility.ts), and the lockfile read that
|
|
4
|
+
// yields the host's versions when the biome index is built. No kernel load, so the index build can import it.
|
|
5
|
+
|
|
6
|
+
/** The id of the biome that ships the adapter host (the service of BiomeComponentHostAbi.AdapterHost). */
|
|
7
|
+
export const ADAPTER_HOST_BIOME_ID = 'biome-adapter-host';
|
|
8
|
+
|
|
9
|
+
/** Exactly the packages xema-connectors tooling/bundle REQUIRED_BUNDLED_PACKAGES names. */
|
|
10
|
+
export const BUNDLED_PACKAGES = Object.freeze(['@xemahq/contracts', '@xemahq/biome-sdk']);
|
|
11
|
+
|
|
12
|
+
const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
13
|
+
|
|
14
|
+
/** The host's rule, copied: same major; when major is 0, same minor too; unparsable is incompatible. */
|
|
15
|
+
export function isCaretCompatible(bundled, host) {
|
|
16
|
+
const left = typeof bundled === 'string' ? VERSION_PATTERN.exec(bundled) : null;
|
|
17
|
+
const right = typeof host === 'string' ? VERSION_PATTERN.exec(host) : null;
|
|
18
|
+
if (left === null || right === null) {return false;}
|
|
19
|
+
if (left[1] !== right[1]) {return false;}
|
|
20
|
+
return left[1] !== '0' || left[2] === right[2];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The host component's resolved versions, read from the repository's pnpm-lock.yaml importer. */
|
|
24
|
+
export function readHostPackageVersions({ lockfileText, importer, lockfilePath, parseYaml }) {
|
|
25
|
+
if (lockfileText === null) {
|
|
26
|
+
throw new Error(`host package versions: ${lockfilePath} does not exist`);
|
|
27
|
+
}
|
|
28
|
+
const dependencies = parseYaml(lockfileText)?.importers?.[importer]?.dependencies;
|
|
29
|
+
if (dependencies === undefined) {
|
|
30
|
+
throw new Error(`host package versions: ${lockfilePath} has no importer "${importer}" with dependencies`);
|
|
31
|
+
}
|
|
32
|
+
const versions = {};
|
|
33
|
+
for (const name of BUNDLED_PACKAGES) {
|
|
34
|
+
const raw = dependencies[name]?.version;
|
|
35
|
+
if (typeof raw !== 'string') {
|
|
36
|
+
throw new Error(`host package versions: ${lockfilePath} importer "${importer}" has no ${name} entry`);
|
|
37
|
+
}
|
|
38
|
+
versions[name] = raw.split('(')[0];
|
|
39
|
+
}
|
|
40
|
+
return versions;
|
|
41
|
+
}
|