@xemahq/xema 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/edition/verify-image-evidence.bundle.mjs +1996 -1012
  2. package/dist/edition/verify-image-evidence.bundle.provenance.json +584 -500
  3. package/dist/runtime/lock-loader.d.ts +2 -1
  4. package/dist/runtime/lock-loader.d.ts.map +1 -1
  5. package/dist/runtime/lock-loader.js +15 -5
  6. package/dist/runtime/lock-loader.js.map +1 -1
  7. package/edition/README.md +8 -3
  8. package/edition/biome-content.mjs +4 -4
  9. package/edition/candidate-edition.mjs +8 -3
  10. package/edition/check-committed-image-evidence.mjs +44 -15
  11. package/edition/customer-edition-verbs.mjs +15 -4
  12. package/edition/develop-current-resolution.mjs +29 -0
  13. package/edition/develop-edition-resolve.mjs +18 -10
  14. package/edition/develop-edition.mjs +30 -9
  15. package/edition/develop-freeze.mjs +24 -1
  16. package/edition/develop-git-bundles.mjs +201 -0
  17. package/edition/develop-graph.mjs +5 -3
  18. package/edition/develop-image-probe.mjs +6 -4
  19. package/edition/develop-image-record.mjs +194 -0
  20. package/edition/develop-record-ancestry.mjs +23 -5
  21. package/edition/develop-select-records.mjs +29 -21
  22. package/edition/develop-selection-line.mjs +30 -0
  23. package/edition/develop-selection-pins.mjs +30 -0
  24. package/edition/dist-check.mjs +45 -9
  25. package/edition/distribution-evidence-set.mjs +14 -9
  26. package/edition/edition-image-seal.mjs +2 -1
  27. package/edition/edition-protocol.mjs +6 -20
  28. package/edition/external-source-walk.mjs +37 -15
  29. package/edition/image-evidence.mjs +6 -2
  30. package/edition/image-lock-service-rows.mjs +4 -4
  31. package/edition/image-subject-identity.mjs +24 -0
  32. package/edition/images-from-record.mjs +46 -110
  33. package/edition/lib.mjs +24 -26
  34. package/edition/locked-bundle-signer.mjs +52 -8
  35. package/edition/resolve-customer-edition.mjs +1 -1
  36. package/edition/resolve-distribution.mjs +84 -21
  37. package/edition/resolve-edition.mjs +2 -1
  38. package/edition/resolve-images.mjs +224 -12
  39. package/edition/seal-image-digests.mjs +24 -13
  40. package/edition/third-party-bundle-registry.mjs +73 -17
  41. package/edition/third-party-source-evidence.mjs +39 -14
  42. package/edition/third-party-source-writer.mjs +2 -18
  43. package/edition/trust-policy-arming.mjs +17 -6
  44. package/edition/verify-image-platforms.mjs +2 -0
  45. package/edition/workspace-sections.mjs +11 -1
  46. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { type DistributionLockReadView, type RuntimeImagePlatform } from '@xemahq/contracts/distribution';
1
+ import { type DistributionLockReadView, type RuntimeImagePlatform, type RuntimeImageSubject } from '@xemahq/contracts/distribution';
2
2
  import { type ArtifactResolution } from './paths';
3
3
  export interface LoadedDistributionLock {
4
4
  readonly lock: DistributionLockReadView;
@@ -20,4 +20,5 @@ export interface LoadedImageLock {
20
20
  readonly resolution: ArtifactResolution;
21
21
  }
22
22
  export declare function loadImageLock(opts: LoadLockOptions): LoadedImageLock;
23
+ export declare function subjectIdentity(subject: RuntimeImageSubject): string;
23
24
  //# sourceMappingURL=lock-loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lock-loader.d.ts","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAE1B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAqD,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAarG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,sBAAsB,CASlF;AAQD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAQ7B,QAAQ,CAAC,iBAAiB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,eAAe;IAQ9B,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1D,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AA0BD,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CAwBpE"}
1
+ {"version":3,"file":"lock-loader.d.ts","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAAqD,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAarG,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,eAAe,GAAG,sBAAsB,CASlF;AAQD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAQ7B,QAAQ,CAAC,iBAAiB,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,eAAe;IAQ9B,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1D,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AA0BD,wBAAgB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,eAAe,CA+BpE;AASD,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAIpE"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadDistributionLock = loadDistributionLock;
4
4
  exports.loadImageLock = loadImageLock;
5
+ exports.subjectIdentity = subjectIdentity;
5
6
  const node_fs_1 = require("node:fs");
6
7
  const distribution_1 = require("@xemahq/contracts/distribution");
7
8
  const paths_1 = require("./paths");
@@ -20,14 +21,21 @@ function loadImageLock(opts) {
20
21
  const lock = (0, distribution_1.parseImageLock)(readJson(resolution.path));
21
22
  const subjectsByKey = new Map();
22
23
  const imagesByService = new Map();
24
+ const subjectsByName = new Map();
23
25
  for (const subject of lock.subjects) {
24
- subjectsByKey.set(subjectKey(subject.kind, subject.id), subject);
26
+ subjectsByKey.set(subjectIdentity(subject), subject);
25
27
  }
26
- for (const [service, subject] of (0, distribution_1.serviceImageSubjects)(lock.subjects)) {
28
+ for (const subject of subjectsByKey.values()) {
29
+ const named = subjectsByName.get(`${subject.kind}:${subject.id}`);
30
+ if (named === undefined || (subject.generation ?? 0) >= (named.generation ?? 0)) {
31
+ subjectsByName.set(`${subject.kind}:${subject.id}`, subject);
32
+ }
33
+ }
34
+ for (const [service, subject] of (0, distribution_1.dedicatedServiceImageSubjects)(lock)) {
27
35
  imagesByService.set(service, toImageEntry(subject));
28
36
  }
29
37
  for (const binding of lock.platformServiceBindings) {
30
- const subject = subjectsByKey.get(subjectKey(binding.subjectKind, binding.subjectId));
38
+ const subject = subjectsByName.get(`${binding.subjectKind}:${binding.subjectId}`);
31
39
  if (subject === undefined) {
32
40
  throw new Error(`Invariant violated reading ${resolution.path}: platform service ` +
33
41
  `"${binding.platformService}" binds the ${binding.subjectKind} subject ` +
@@ -37,8 +45,10 @@ function loadImageLock(opts) {
37
45
  }
38
46
  return { imagesByService, resolution };
39
47
  }
40
- function subjectKey(kind, id) {
41
- return `${kind}:${id}`;
48
+ function subjectIdentity(subject) {
49
+ return subject.kind === distribution_1.RuntimeImageSubjectKind.ComposedProcess
50
+ ? `${subject.kind}:${subject.imageRef}:${subject.contentHash}`
51
+ : `${subject.kind}:${subject.id}`;
42
52
  }
43
53
  function toImageEntry(subject) {
44
54
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"lock-loader.js","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lock-loader.js","sourceRoot":"","sources":["../../src/runtime/lock-loader.ts"],"names":[],"mappings":"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"}
package/edition/README.md CHANGED
@@ -82,7 +82,8 @@ xema edition artifact <id> --channel release --registry <ns> --coordinate <tag>
82
82
  xema edition develop <freeze-sources|resolve|target|check-existing> ...
83
83
  # develop tag = sha256 of { cli, engine, pins, sourceImages }; target and check-existing take --engine-image, and a customer's --source-image
84
84
  xema edition develop coverage <id> --pins <file> --cosign <abs path> [--json] # read-only forecast (policy held to its pin, this CLI's own verifier code): is each develop image producer-signed? exit 0 covered, 1 not, 2 error
85
- xema edition develop select-records --branch develop --registry <root> --cosign <abs path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>] # newest signed build record per graph member (the newest COMMIT on develop, every record verified; --published-pins keeps a published commit the records are not ahead of, part 07 passes it) -> { pins, snapshot, images }, written once; any bad record refuses the run (needs dataRootRepositoryUrl in edition-workspace.json; anyone who can delete registry tags can remove newer records and this then pins the newest remaining valid one, and the guard is part 07's: deploy-latest's owner refuses a snapshot that goes backwards)
85
+ xema edition develop select-records --branch develop --registry <root> --cosign <abs path> --out <file> [--at <ISO-8601 UTC>] [--published-pins <file>] # newest signed build record per graph member (the newest COMMIT on develop, every record verified; --published-pins keeps a published commit the records are not ahead of, part 07 passes it) -> { pins, snapshot, behind, images, currentResolution }, written once (a member whose firstPartySources entry declares `currentResolution: "<ticket>"`, xema-docs until XEMA-2373, is skipped with no registry read, listed in currentResolution and printed as `xema-docs: current resolution (leaves the edition, XEMA-2373)`) (behind = { <repo>: { head, commits } }: develop's head and the commits the pinned one trails it by; stdout gets one `<short name>: record <commit>, <N> commits behind HEAD` line per member, always `commits`, short name = the repo's last path segment and unambiguous; images[] entries carry `member`); any bad record refuses the run (needs dataRootRepositoryUrl in edition-workspace.json; anyone who can delete registry tags can remove newer records and this then pins the newest remaining valid one, and the guard is part 07's: deploy-latest's owner refuses a snapshot that goes backwards)
86
+ xema edition develop image-record <id> --selection <file> --pins <file> --out <file> # the edition's image build record (what `resolve --images-from` reads) from a select-records selection: subjects are the checked-out snapshot's first-party image subjects; a current-resolution member's (xema-docs) is probed and goes in `images`, every other takes the selection's image of its develop repository (selected commit must equal the snapshot's) and goes in `satellites`; platforms read by digest; any gap refuses and writes nothing; written once
86
87
  xema edition develop verify-image-record --image <name>@sha256:<digest> --producer <owner/repo> --registry <root> --cosign <abs path> # is the digest bound to the producer by an authenticated develop build record (shared verify core with select-records). Records are read newest run first (listBuildRecordRuns sorts by compareBuildRecordRuns: run, then attempt, highest first), a verified record of a workflow other than the producer's `.github/workflows/ci.yaml` is skipped, and the answer is the newest ci.yaml record listing the exact image -> { image, digest, producer, commit }. `commit` is the commit of the newest verified ci.yaml record (by run number, then attempt) that lists the digest; it can be an older commit when a later run targeted it, and for an image the producer reused by content hash it is NOT the commit that built it. This is safe because the digest is what binds the bytes. Refuses when no ci.yaml record lists the image, the record names another producer, or cosign refuses the identity
87
88
  xema edition develop check-overrides --overrides <json> --data-root <dir> # data root only
88
89
  xema edition ceiling --distribution <id> --at <distributions sha> --producer-commit <owner/repo>=<sha> [...] --data-root <dir> --sources-root <dir> --kernel <dir> # the dev ceiling (DevelopCeilingSchema from @xemahq/contracts/develop-ceiling): selection, origin, repo and trustCeiling from the definition and indexes at --at; version, services, runtimeComponents and serviceProcessFacts from each producer's xema-biome.json and xema-biome.index.json at its --producer-commit; a missing commit or manifest refuses, with no fallback
@@ -126,7 +127,7 @@ The develop graph is the data root's first-party repositories, declared in `edit
126
127
 
127
128
  - Each key is a repository handle (`repos/<x>` or `submodules/<x>`); `sourceId` is its kebab-case source id; `repositoryUrl` is canonical HTTPS under `sourceRepositoryUrlPrefix`. Both are unique. The retired string form (`"repos/x": "x"`) is refused with `RetiredFirstPartySourceShapeError`.
128
129
  - `imageOrigin` says where the source's images come from on the everyday lane: `workspace` (built by the job in the aggregator workspace) or `satellite` (a producer outside it, such as `submodules/xema-host-web`, that publishes its own signed develop digest). The parse accepts a source without it and fills in no default; `--images-from` refuses such a source (see below).
129
- - `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
130
+ - `xema edition develop freeze-graph` pins every declared repository at the `develop` head of its declared URL; overrides may name any of them. With `--member-pins <selection file>` (a select-records selection; only `.pins`, 40-hex, is read) each member is pinned at its selected record commit instead: the selection must pin exactly the graph minus its `currentResolution` members (equal to the graph members' declared `currentResolution`; a missing or extra member is refused by name), a `currentResolution` member (xema-docs, XEMA-2373) is pinned at its develop head, and `--member-pins` with `--overrides` is refused. The snapshot's `repositoryPins` (`develop resolve`, `checkout-indexed-source-repositories.mjs --snapshot-pins`) are exactly these repositories. The runtime image source index's `repositoryPins` are release pins and are not read on develop.
130
131
  - A first-party repository is in the develop graph before any release pins it. The converse is checked offline: every `repositoryPins` entry must be declared first-party at the same `repositoryUrl`.
131
132
 
132
133
  ## Develop images from a build record
@@ -177,9 +178,13 @@ The edition library builds, validates and verifies evidence statements; it never
177
178
  - `xema edition evidence statement catalogue stage|accept` (vendor) derives the E4 statement from `--artifact oci://<repository>@sha256:<digest>` and `--offering <id>`, and `accept` attaches the signed statement and bundle to the artifact as a referrer. A release catalogue is verified before it is read, by the data root's pinned built verifier (`catalogue-evidence arming|verify` mode of the bundle, which uses its own embedded kernel because the catalogue's kernel is not adopted yet); `xema edition release resolve` and `check-existing` take `--cosign <absolute path>` for it.
178
179
  - `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.
179
180
 
181
+ ## Previous seal
182
+
183
+ A write of `images.lock.json` (`resolve-images`, `xema edition develop resolve`) takes exactly one of `--previous-lock <lock> --previous-lock-sha256 <hex>` or `--first-seal`; the CLI refuses unless the lock's bytes hash to the digest, which the caller takes from the images.lock layer of the signed, published edition (the latest, under the per-edition concurrency group). The in-repo resolve and pin/unpin take the committed `images.lock.json` at the target path as the previous lock (first seal when absent). The the composed artifacts' generations carry forward from it.
184
+
180
185
  ## Kernel protocol and pins
181
186
 
182
- 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 protocols v28, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39 and v40 (no published kernel declares v29; a CLI lists a protocol only while every kernel published under it exports what the CLI imports; v27 and older kernels lack `serviceImageSubjects`, so a data root whose pinned kernel speaks v27 or older stays on an earlier CLI), pinned to its digest and vendored, 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.
187
+ 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 protocols v28, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40 and v41 (no published kernel declares v29; a CLI lists a protocol only while every kernel published under it exports what the CLI imports; v27 and older kernels lack `serviceImageSubjects`, so a data root whose pinned kernel speaks v27 or older stays on an earlier CLI), pinned to its digest and vendored, 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.
183
188
 
184
189
  A develop edition does not take the kernel's develop head blindly: `xema edition develop choose-kernel --kernel <package dir> [--kernel-pin <40-hex>] --out <file>` reads the declared protocol of each commit on the checkout's first-parent history (from source, unbuilt) and chooses the newest one this CLI supports. It writes `{chosenSha, headSha, held, reason}` (`reason`: `head-supported` | `head-ahead` | `explicit-pin`; `held` is `head-ahead`), and when held prints `kernel held at v<N> (<sha12>), head at v<M> (<sha12>): raise the CLI` (stdout only, never persisted). The head is the checkout's `refs/remotes/origin/develop` (a full-history checkout). An explicit pin wins but is still checked, and must be on that head's first-parent history (`not-on-develop` otherwise); when no commit is supported it refuses (`EditionProtocolMismatchError`, `no-supported-commit`; the walk ends at the first commit with no declaration file) and never falls back to the head; a declaration that exists but does not parse, at the head or any walked commit, is refused by name (`unparseable`), never skipped. `freeze-graph` applies the same check to `--kernel-sha`.
185
190
 
@@ -291,8 +291,8 @@ export async function biomeSourceTreeIds({ schemas, lock, imageLock, locate, ven
291
291
  }
292
292
 
293
293
  /** Seal: the unsealed lock + the recomputed hashes -> the kernel's `sealDistributionLock`. */
294
- export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, sourceFactsById, bundleSigners, contentCommits }) {
295
- assertLockBundleSigners({ lock: unsealed, selection: bundleSigners, schemas });
294
+ export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, sourceFactsById, bundleSigners, contentCommits, gitSourcedBundle }) {
295
+ assertLockBundleSigners({ lock: unsealed, selection: bundleSigners, schemas, gitSourcedBundle });
296
296
  const hashes = await computeBiomeContentHashes({ schemas, lock: unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, sourceFactsById, contentCommits });
297
297
  admitDistributionLock(schemas, unsealed);
298
298
  return schemas.sealDistributionLock(unsealed, hashes);
@@ -302,9 +302,9 @@ export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredC
302
302
  * Verify: recompute every biome's hash and REFUSE a mismatch, naming the biome.
303
303
  * The lock's own field is never trusted; a missing or extra biome is a mismatch.
304
304
  */
305
- export async function verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners, contentCommits }) {
305
+ export async function verifyBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, bundleSigners, contentCommits, gitSourcedBundle }) {
306
306
  const expected = await computeBiomeContentHashes({ schemas, lock, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, contentCommits });
307
- assertLockBundleSigners({ lock, selection: bundleSigners, schemas });
307
+ assertLockBundleSigners({ lock, selection: bundleSigners, schemas, gitSourcedBundle });
308
308
  const mismatched = lock.biomes
309
309
  .filter((biome) => biome.biomeContentHash !== expected.get(biome.id))
310
310
  .map((biome) => `${biome.id} (lock ${String(biome.biomeContentHash)}, recomputed ${expected.get(biome.id)})`);
@@ -38,6 +38,7 @@ import {
38
38
  sourceCheckoutPlan,
39
39
  } from './checkout-indexed-source-repositories.mjs';
40
40
  import { assertDevelopEditionInvariants } from './develop-edition-invariants.mjs';
41
+ import { carriedDevelopGitBundles } from './develop-git-bundles.mjs';
41
42
  import { buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
42
43
  import { parseEditionDescriptor } from './edition-artifact.mjs';
43
44
  import { resetEditionWorkspaceConfig, editionWorkspaceConfig } from './edition-config.mjs';
@@ -55,7 +56,7 @@ import {
55
56
  import { materializeGitSource, externalSourceCacheDir } from './external-source-walk.mjs';
56
57
  import { withSourceGitEnv } from './git-source-auth.mjs';
57
58
  import { loadKernelDistributionSchemas, serializeJson, sha256Hex } from './lib.mjs';
58
- import { assertLockBundleSigners } from './locked-bundle-signer.mjs';
59
+ import { GitSourcedBundle, assertLockBundleSigners } from './locked-bundle-signer.mjs';
59
60
  import { admitDistributionLock } from './operation-grant-admission.mjs';
60
61
  import {
61
62
  EDITIONS_ROOT_PIN_KEY,
@@ -581,8 +582,10 @@ async function resealStaged({ edition: pinned, pins, read, deployment }) {
581
582
  const id = pinned.id;
582
583
  const schemas = await loadKernelDistributionSchemas();
583
584
  const inputs = await buildSnapshotInputs({ id, pins, schemas });
584
- const { lock, index } = await prepareDistributionWithIndex(id, { inputs });
585
- assertLockBundleSigners({ lock, schemas });
585
+ // A git-sourced biome's OCI bundle is carried from the pinned edition for the same source commit, never re-fetched.
586
+ const pinnedLock = JSON.parse(pinned.bytes[EditionLockFile.Distribution].toString('utf8'));
587
+ const { lock, index } = await prepareDistributionWithIndex(id, { inputs: { ...inputs, developGitBundles: carriedDevelopGitBundles(pinnedLock) } });
588
+ assertLockBundleSigners({ lock, schemas, gitSourcedBundle: GitSourcedBundle.DevelopVerified });
586
589
  assertDevelopEditionInvariants({ id, lock, biomeIndex: inputs.biomeIndex, schemas });
587
590
  admitDistributionLock(schemas, lock);
588
591
  const authorizationLock = await composeAuthorizationBase(id, lock, index);
@@ -600,6 +603,7 @@ async function resealStaged({ edition: pinned, pins, read, deployment }) {
600
603
  locate: sourceTreeLocator(externalSourceTrees(lock)),
601
604
  vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
602
605
  contentCommits,
606
+ gitSourcedBundle: GitSourcedBundle.DevelopVerified,
603
607
  });
604
608
  const lockBytes = Buffer.from(serializeJson(sealed), 'utf8');
605
609
  const authorizationLockBytes = Buffer.from(serializeJson(authorizationLock), 'utf8');
@@ -684,6 +688,7 @@ export async function verifyCandidateContentHashes({ tripleDir, checkouts, candi
684
688
  locate: sourceTreeLocator(externalSourceTrees(lock)),
685
689
  vendoredCommit: (repo) => authority.vendoredSourceCommitForRepository(repo),
686
690
  ...(contentCommits === undefined ? {} : { contentCommits }),
691
+ gitSourcedBundle: GitSourcedBundle.DevelopVerified,
687
692
  });
688
693
  });
689
694
  } finally {
@@ -66,6 +66,7 @@ import {
66
66
  evidenceDirectoryForDigest,
67
67
  sha256Digest,
68
68
  } from './image-evidence.mjs';
69
+ import { evidenceSubjectKey } from './image-subject-identity.mjs';
69
70
  import { loadKernelDistributionSchemas } from './lib.mjs';
70
71
  import {
71
72
  assertProducerInput,
@@ -124,6 +125,29 @@ function assertContainedRelativePath(value, label) {
124
125
  *
125
126
  * @returns {{ evidenceDirectory: string, reusable: boolean, reason: string }}
126
127
  */
128
+ /**
129
+ * The key of an index entry: its subject's evidence identity, which for a composed artifact names the generation, so it is read
130
+ * from the statement the entry points at (the index states only kind and id). An entry whose statement cannot be read keeps
131
+ * `kind\0id`; the unusable statement is reported below.
132
+ */
133
+ export function entryKey(entry, absoluteDirectory) {
134
+ try {
135
+ const { predicate } = JSON.parse(
136
+ readBoundedFile(join(absoluteDirectory, assertContainedRelativePath(entry.evidence.statementPath, 'statementPath'))).toString('utf8'),
137
+ );
138
+ return evidenceSubjectKey({
139
+ kind: entry.subjectKind,
140
+ id: entry.subjectId,
141
+ imageRef: predicate.artifact.imageRepository,
142
+ contentHash: predicate.source.sourceTreeHash,
143
+ });
144
+ } catch {
145
+ return `${entry.subjectKind}\0${entry.subjectId}`;
146
+ }
147
+ }
148
+
149
+ const printable = (key) => key.replaceAll('\0', ':');
150
+
127
151
  export async function assessCommittedEvidence({
128
152
  input,
129
153
  repositoryRoot,
@@ -190,7 +214,12 @@ export async function assessCommittedEvidence({
190
214
  }
191
215
  const expectedByKey = new Map(
192
216
  expectedStatements.map((statement) => [
193
- `${statement.predicate.source.kind}:${statement.predicate.source.id}`,
217
+ evidenceSubjectKey({
218
+ kind: statement.predicate.source.kind,
219
+ id: statement.predicate.source.id,
220
+ imageRef: statement.predicate.artifact.imageRepository,
221
+ contentHash: statement.predicate.source.sourceTreeHash,
222
+ }),
194
223
  statement,
195
224
  ]),
196
225
  );
@@ -200,20 +229,20 @@ export async function assessCommittedEvidence({
200
229
  if (entry === null || typeof entry !== 'object') {
201
230
  return unusable('producer-evidence.json carries a non-object entry');
202
231
  }
203
- const key = `${entry.subjectKind}:${entry.subjectId}`;
232
+ const key = entryKey(entry, absoluteDirectory);
204
233
  if (seen.has(key)) {
205
- return unusable(`producer-evidence.json duplicates subject ${key}`);
234
+ return unusable(`producer-evidence.json duplicates subject ${printable(key)}`);
206
235
  }
207
236
  seen.add(key);
208
237
  const expected = expectedByKey.get(key);
209
238
  if (expected === undefined) {
210
239
  return unusable(
211
- `committed evidence carries subject ${key}, which this input does not evidence`,
240
+ `committed evidence carries subject ${printable(key)}, which this input does not evidence`,
212
241
  );
213
242
  }
214
243
  const descriptor = entry.evidence;
215
244
  if (descriptor === null || typeof descriptor !== 'object') {
216
- return unusable(`subject ${key} has no evidence descriptor`);
245
+ return unusable(`subject ${printable(key)} has no evidence descriptor`);
217
246
  }
218
247
  let statementBytes;
219
248
  let bundleBytes;
@@ -223,7 +252,7 @@ export async function assessCommittedEvidence({
223
252
  absoluteDirectory,
224
253
  assertContainedRelativePath(
225
254
  descriptor.statementPath,
226
- `subject ${key} statementPath`,
255
+ `subject ${printable(key)} statementPath`,
227
256
  ),
228
257
  ),
229
258
  );
@@ -232,25 +261,25 @@ export async function assessCommittedEvidence({
232
261
  absoluteDirectory,
233
262
  assertContainedRelativePath(
234
263
  descriptor.bundlePath,
235
- `subject ${key} bundlePath`,
264
+ `subject ${printable(key)} bundlePath`,
236
265
  ),
237
266
  ),
238
267
  );
239
268
  } catch (error) {
240
- return unusable(`subject ${key} artifacts are unreadable: ${error.message}`);
269
+ return unusable(`subject ${printable(key)} artifacts are unreadable: ${error.message}`);
241
270
  }
242
271
  if (
243
272
  sha256Digest(statementBytes) !== descriptor.statementDigest ||
244
273
  sha256Digest(bundleBytes) !== descriptor.bundleDigest
245
274
  ) {
246
275
  return unusable(
247
- `subject ${key} artifacts do not match the digests producer-evidence.json records`,
276
+ `subject ${printable(key)} artifacts do not match the digests producer-evidence.json records`,
248
277
  );
249
278
  }
250
279
  try {
251
280
  validateProducedBundle(statementBytes, bundleBytes);
252
281
  } catch (error) {
253
- return unusable(`subject ${key} bundle is malformed: ${error.message}`);
282
+ return unusable(`subject ${printable(key)} bundle is malformed: ${error.message}`);
254
283
  }
255
284
  let committed;
256
285
  try {
@@ -259,7 +288,7 @@ export async function assessCommittedEvidence({
259
288
  );
260
289
  } catch (error) {
261
290
  return unusable(
262
- `subject ${key} statement does not parse under the kernel schema: ${error.message}`,
291
+ `subject ${printable(key)} statement does not parse under the kernel schema: ${error.message}`,
263
292
  );
264
293
  }
265
294
  // producerRun is deliberately fresh on every execution (run id, attempt,
@@ -269,24 +298,24 @@ export async function assessCommittedEvidence({
269
298
  // validate the existing run's own signature and lifetime independently.
270
299
  if (!producerStatementMatchesInput(committed, expected)) {
271
300
  return unusable(
272
- `subject ${key} committed statement is not bound to this producer input ` +
301
+ `subject ${printable(key)} committed statement is not bound to this producer input ` +
273
302
  '(authority, source, build, artifact or platform set changed)',
274
303
  );
275
304
  }
276
305
  const expiresAt = Date.parse(committed.predicate.producerRun.expiresAt);
277
306
  if (!Number.isFinite(expiresAt)) {
278
- return unusable(`subject ${key} statement has an unreadable expiresAt`);
307
+ return unusable(`subject ${printable(key)} statement has an unreadable expiresAt`);
279
308
  }
280
309
  const remainingSeconds = Math.floor((expiresAt - now) / 1000);
281
310
  if (remainingSeconds <= 0) {
282
311
  return unusable(
283
- `subject ${key} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
312
+ `subject ${printable(key)} evidence EXPIRED at ${committed.predicate.producerRun.expiresAt} — ` +
284
313
  'it can never verify again and must be re-produced',
285
314
  );
286
315
  }
287
316
  if (remainingSeconds < MIN_REMAINING_TTL_SECONDS) {
288
317
  return unusable(
289
- `subject ${key} evidence expires in ${remainingSeconds}s, under the ` +
318
+ `subject ${printable(key)} evidence expires in ${remainingSeconds}s, under the ` +
290
319
  `${MIN_REMAINING_TTL_SECONDS}s reuse floor — refreshing it now`,
291
320
  );
292
321
  }
@@ -183,15 +183,20 @@ const ENGINE_IMAGE_FLAG = '--engine-image';
183
183
  const ENGINE_REPOSITORY_FLAG = '--engine-repository';
184
184
  const ENGINE_SIGNER_FLAG = '--engine-signer';
185
185
  export const IMAGES_FROM_FLAG = '--images-from';
186
+ export const PREVIOUS_LOCK_FLAG = '--previous-lock';
187
+ export const PREVIOUS_LOCK_SHA256_FLAG = '--previous-lock-sha256';
188
+ export const FIRST_SEAL_FLAG = '--first-seal';
186
189
 
187
190
  const COMMANDS = Object.freeze({
188
191
  resolve: {
189
192
  positional: true,
190
193
  // --images-from: the everyday job's image build record (images-from-record.mjs) instead of the registry probe; vendor only.
191
- values: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, IMAGES_FROM_FLAG],
192
- switches: ['--local'],
194
+ // --previous-lock with --previous-lock-sha256 (the digest of the latest signed, published edition's images.lock layer, passed by the workflow under
195
+ // the per-edition concurrency group) or --first-seal: the previous seal whose composed artifacts carry their generation forward; vendor only.
196
+ values: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG, IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG],
197
+ switches: ['--local', FIRST_SEAL_FLAG],
193
198
  required: ['--pins', '--out', '--registry', ENGINE_REPOSITORY_FLAG, ENGINE_SIGNER_FLAG, COSIGN_FLAG],
194
- customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG], cosign: true },
199
+ customer: { values: [DEFINITION_FLAG, ...CREDENTIAL_FLAGS, SOURCE_IMAGE_FLAG, COSIGN_FLAG, ENGINE_IMAGE_FLAG], required: ['--pins', '--out', '--registry', '--engine-image'], refused: [IMAGES_FROM_FLAG, PREVIOUS_LOCK_FLAG, PREVIOUS_LOCK_SHA256_FLAG, FIRST_SEAL_FLAG], cosign: true },
195
200
  // A vendor resolve finds its engine itself; the old flag is refused by name, never read as a customer flag.
196
201
  removedFromVendor: { [ENGINE_IMAGE_FLAG]: `use ${ENGINE_REPOSITORY_FLAG} and ${ENGINE_SIGNER_FLAG}` },
197
202
  },
@@ -210,7 +215,7 @@ const COMMANDS = Object.freeze({
210
215
  },
211
216
  'freeze-graph': {
212
217
  positional: false,
213
- values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--out'],
218
+ values: ['--data-sha', '--kernel-sha', '--deploy-sha', '--editions-root-url', '--overrides', '--member-pins', '--out'],
214
219
  switches: [],
215
220
  required: ['--data-sha', '--kernel-sha', '--deploy-sha', '--out'],
216
221
  },
@@ -233,6 +238,12 @@ const COMMANDS = Object.freeze({
233
238
  switches: [],
234
239
  required: ['--branch', '--registry', '--cosign', '--out'],
235
240
  },
241
+ 'image-record': {
242
+ positional: true,
243
+ values: ['--selection', '--pins', '--out'],
244
+ switches: [],
245
+ required: ['--selection', '--pins', '--out'],
246
+ },
236
247
  'verify-image-record': {
237
248
  positional: false,
238
249
  values: ['--image', '--producer', '--registry', '--cosign'],
@@ -0,0 +1,29 @@
1
+ // develop-current-resolution — the members that keep the CURRENT develop resolution instead of a signed build record.
2
+ //
3
+ // A member keeps its current resolution (develop HEAD freeze plus the registry probe) only when the data root declares
4
+ // it: `currentResolution: "<ticket>"` on its `firstPartySources` entry in edition-workspace.json, the ticket that
5
+ // removes the exception (today xema-docs, whose develop lane is unsigned by owner decision and which leaves the
6
+ // edition). It is a declared exception, not a fallback: absent means none, and every other member without a record
7
+ // still refuses.
8
+
9
+ import { compareStrings } from './lib.mjs';
10
+
11
+ /** Whether a member (or its `firstPartySources` entry) keeps its current resolution. */
12
+ export const keepsCurrentResolution = (member) => Object.hasOwn(member, 'currentResolution');
13
+
14
+ /** The graph `members` that declare a current resolution, repo -> ticket, keys sorted: what a selection records. */
15
+ export function currentResolutionOf(members) {
16
+ return Object.fromEntries(
17
+ members
18
+ .filter(keepsCurrentResolution)
19
+ .map(({ repo, currentResolution }) => [repo, currentResolution])
20
+ .sort(([a], [b]) => compareStrings(a, b)),
21
+ );
22
+ }
23
+
24
+ /** Why a selection's `currentResolution` (absent = none) is not exactly the one the graph `members` declare; null when it is. */
25
+ export function currentResolutionProblem(declared, members) {
26
+ const expected = currentResolutionOf(members);
27
+ if (JSON.stringify(declared ?? {}) === JSON.stringify(expected)) {return null;}
28
+ return `"currentResolution" is ${JSON.stringify(declared)}, not ${JSON.stringify(expected)}, the members the data root declares keep their current resolution`;
29
+ }
@@ -13,6 +13,7 @@ import { buildCatalogue } from './catalogue.mjs';
13
13
  import { resolveDevelopChartSet } from './develop-chart-set.mjs';
14
14
  import { assertDevelopEditionInvariants } from './develop-edition-invariants.mjs';
15
15
  import { resolveDevelopEngineImage } from './develop-engine-image.mjs';
16
+ import { createDevelopGitBundleVerifier } from './develop-git-bundles.mjs';
16
17
  import { createDevelopImageProbe } from './develop-image-probe.mjs';
17
18
  import { BLOCKED, DevelopResolveCheck, RESOLVE_CHECK_DEPENDENCIES, createResolveReport } from './develop-resolve-report.mjs';
18
19
  import { assertFrozenCheckouts, buildSnapshotInputs } from './develop-snapshot-inputs.mjs';
@@ -23,7 +24,7 @@ import { dataRoot, kernelPackageDir } from './edition-workspace.mjs';
23
24
  import { resolveImagesFromRecord, sealImagesFromRecord } from './images-from-record.mjs';
24
25
  import { kernelPackageOfAuthority } from './kernel-package.mjs';
25
26
  import { loadKernelDistributionSchemas } from './lib.mjs';
26
- import { assertLockBundleSigners, isBundleSignerRefusal } from './locked-bundle-signer.mjs';
27
+ import { GitSourcedBundle, assertLockBundleSigners, isBundleSignerRefusal } from './locked-bundle-signer.mjs';
27
28
  import { admitDistributionLock, ungrantedOptionalLinesOfRefusal } from './operation-grant-admission.mjs';
28
29
  import { deployPinKey } from './pin-source.mjs';
29
30
  import { admitResolvedLock, composeAuthorizationBase, prepareDistributionWithIndex } from './resolve-distribution.mjs';
@@ -58,17 +59,21 @@ const stderrProgress = (line) => process.stderr.write(`${line}\n`);
58
59
  * admits it as one of its checks; `resolveDevelopSnapshot` admits it at once. Both
59
60
  * start here, so the content hash each computes for a subject is the same by construction.
60
61
  */
61
- async function prepareDevelopSnapshot({ id, pins, steps, phase }) {
62
+ async function prepareDevelopSnapshot({ id, pins, steps, phase, developGitBundles }) {
62
63
  const schemas = await loadKernelDistributionSchemas();
63
64
  steps.assertFrozenCheckouts(pins);
64
65
  const inputs = await steps.buildSnapshotInputs({ id, pins, schemas });
65
66
  phase('snapshot inputs built');
66
- const prepared = await steps.prepareDistributionWithIndex(id, { inputs });
67
+ const prepared = await steps.prepareDistributionWithIndex(id, { inputs: developGitBundles === undefined ? inputs : { ...inputs, developGitBundles } });
67
68
  // The schemas the lock was composed with are the ones that admit it.
68
69
  return { inputs, ...prepared };
69
70
  }
70
71
 
71
- /** The read-only `coverage` verb's snapshot: prepared as the resolve prepares it, then admitted (a refusal throws). */
72
+ /**
73
+ * The read-only `coverage` and image-record snapshot: prepared as the resolve prepares it, then admitted (a refusal
74
+ * throws). It seals no distribution lock, so it verifies no git-sourced OCI bundle (no registry, no cosign): its lock
75
+ * rows of a git source carry none, which neither verb reads.
76
+ */
72
77
  export async function resolveDevelopSnapshot({ id, pins, steps = DEVELOP_EDITION_STEPS, phase = () => {} }) {
73
78
  const snapshot = await prepareDevelopSnapshot({ id, pins, steps, phase });
74
79
  admitResolvedLock(snapshot.schemas, snapshot.lock);
@@ -79,8 +84,9 @@ export async function resolveDevelopSnapshot({ id, pins, steps = DEVELOP_EDITION
79
84
  * @param {object} request
80
85
  * @param {object} [request.imagesFrom] a parsed image build record (images-from-record.mjs): the digests are the record's,
81
86
  * with no probe and no rehash, and `verifySatellites` (createSatelliteVerifier) admits its satellite entries
87
+ * @param {object|null} [request.previousLock] the previous edition's parsed images lock: its composed artifacts carry their generation forward (null: generation 1)
82
88
  */
83
- export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites }) {
89
+ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, env = process.env, sealOptions = {}, fetchKernelTarball, fetchManifest = fetchRegistryManifest, progress = stderrProgress, steps = DEVELOP_EDITION_STEPS, imagesFrom, verifySatellites, previousLock = null }) {
84
90
  if ((imagesFrom === undefined) !== (verifySatellites === undefined)) {
85
91
  throw new Error('resolveDevelopEdition takes an image build record and its satellite verifier together, or neither');
86
92
  }
@@ -101,13 +107,15 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
101
107
  return image;
102
108
  });
103
109
  await checked(DevelopResolveCheck.EditionProtocol, () => steps.assertKernelEditionProtocol(kernelPackageDir()));
104
- const snapshot = await checked(DevelopResolveCheck.DistributionLock, () => prepareDevelopSnapshot({ id, pins, steps, phase }), {
110
+ // A git-sourced biome's OCI bundle is kept only when verified against the frozen source (develop-git-bundles.mjs).
111
+ const developGitBundles = createDevelopGitBundleVerifier({ cosignBinary, dataRoot: dataRoot(), env });
112
+ const snapshot = await checked(DevelopResolveCheck.DistributionLock, () => prepareDevelopSnapshot({ id, pins, steps, phase, developGitBundles }), {
105
113
  classify: (error) => (isBundleSignerRefusal(error) ? DevelopResolveCheck.BundleSigner : DevelopResolveCheck.DistributionLock),
106
114
  });
107
115
  const lockProduced = snapshot !== BLOCKED;
108
116
  const { schemas, inputs, lock, index } = lockProduced ? snapshot : {};
109
117
  if (lockProduced) {
110
- await checked(DevelopResolveCheck.BundleSigner, () => assertLockBundleSigners({ lock, schemas }));
118
+ await checked(DevelopResolveCheck.BundleSigner, () => assertLockBundleSigners({ lock, schemas, gitSourcedBundle: GitSourcedBundle.DevelopVerified }));
111
119
  }
112
120
  // The plane invariants, against the graph this candidate froze (develop-edition-invariants.mjs).
113
121
  await checked(DevelopResolveCheck.PlaneInvariants, () => {
@@ -135,7 +143,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
135
143
  let recorded;
136
144
  const imageLock = await checked(DevelopResolveCheck.DevelopImages, async () => {
137
145
  if (imagesFrom !== undefined) {
138
- const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites });
146
+ const fromRecord = await resolveImagesFromRecord({ id, inputs, lock, record: imagesFrom, resolveImages: steps.resolveImages, verifySatellites, previousLock });
139
147
  ({ recorded } = fromRecord);
140
148
  phase(`image subjects taken from the build record (${fromRecord.imageLock.subjects.length} subjects); satellites verified`);
141
149
  return fromRecord.imageLock;
@@ -148,7 +156,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
148
156
  ...(sealOptions.run === undefined ? {} : { run: sealOptions.run }),
149
157
  progress,
150
158
  });
151
- const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe });
159
+ const resolved = await steps.resolveImages(id, { inputs, lock, onSubjectHashed: probe.observe, previousLock });
152
160
  phase(`image content hashes computed (${resolved.imageLock.subjects.length} subjects)`);
153
161
  await probe.settle();
154
162
  phase('every develop image is present');
@@ -178,7 +186,7 @@ export async function resolveDevelopEdition({ id, pins, engine, cosignBinary, en
178
186
  // ONE walk of the biome checkouts: the content hashes and the tree ids both read it.
179
187
  const sourceFactsById = await readBiomeSourceFacts({ ...biomeSources, lock });
180
188
  phase(`biome sources read (${sourceFactsById.size} biomes)`);
181
- const sealedLock = await sealLock({ ...biomeSources, unsealed: lock, sourceFactsById });
189
+ const sealedLock = await sealLock({ ...biomeSources, unsealed: lock, sourceFactsById, gitSourcedBundle: GitSourcedBundle.DevelopVerified });
182
190
  phase('distribution lock sealed');
183
191
  const sourceTreeIds = await biomeSourceTreeIds({ ...biomeSources, lock, sourceFactsById });
184
192
  const catalogue = buildCatalogue({