@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.
- package/dist/edition/verify-image-evidence.bundle.mjs +1996 -1012
- package/dist/edition/verify-image-evidence.bundle.provenance.json +584 -500
- package/dist/runtime/lock-loader.d.ts +2 -1
- package/dist/runtime/lock-loader.d.ts.map +1 -1
- package/dist/runtime/lock-loader.js +15 -5
- package/dist/runtime/lock-loader.js.map +1 -1
- package/edition/README.md +8 -3
- package/edition/biome-content.mjs +4 -4
- package/edition/candidate-edition.mjs +8 -3
- package/edition/check-committed-image-evidence.mjs +44 -15
- package/edition/customer-edition-verbs.mjs +15 -4
- package/edition/develop-current-resolution.mjs +29 -0
- package/edition/develop-edition-resolve.mjs +18 -10
- package/edition/develop-edition.mjs +30 -9
- package/edition/develop-freeze.mjs +24 -1
- package/edition/develop-git-bundles.mjs +201 -0
- package/edition/develop-graph.mjs +5 -3
- package/edition/develop-image-probe.mjs +6 -4
- package/edition/develop-image-record.mjs +194 -0
- package/edition/develop-record-ancestry.mjs +23 -5
- package/edition/develop-select-records.mjs +29 -21
- package/edition/develop-selection-line.mjs +30 -0
- package/edition/develop-selection-pins.mjs +30 -0
- package/edition/dist-check.mjs +45 -9
- package/edition/distribution-evidence-set.mjs +14 -9
- package/edition/edition-image-seal.mjs +2 -1
- package/edition/edition-protocol.mjs +6 -20
- package/edition/external-source-walk.mjs +37 -15
- package/edition/image-evidence.mjs +6 -2
- package/edition/image-lock-service-rows.mjs +4 -4
- package/edition/image-subject-identity.mjs +24 -0
- package/edition/images-from-record.mjs +46 -110
- package/edition/lib.mjs +24 -26
- package/edition/locked-bundle-signer.mjs +52 -8
- package/edition/resolve-customer-edition.mjs +1 -1
- package/edition/resolve-distribution.mjs +84 -21
- package/edition/resolve-edition.mjs +2 -1
- package/edition/resolve-images.mjs +224 -12
- package/edition/seal-image-digests.mjs +24 -13
- package/edition/third-party-bundle-registry.mjs +73 -17
- package/edition/third-party-source-evidence.mjs +39 -14
- package/edition/third-party-source-writer.mjs +2 -18
- package/edition/trust-policy-arming.mjs +17 -6
- package/edition/verify-image-platforms.mjs +2 -0
- package/edition/workspace-sections.mjs +11 -1
- 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,
|
|
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(
|
|
26
|
+
subjectsByKey.set(subjectIdentity(subject), subject);
|
|
25
27
|
}
|
|
26
|
-
for (const
|
|
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 =
|
|
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
|
|
41
|
-
return
|
|
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":";;AAoCA,oDASC;AAyDD,
|
|
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
|
|
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
|
-
|
|
585
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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({
|