@xemahq/xema 0.29.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -389
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/edition.js +3 -3
- package/dist/commands/edition.js.map +1 -1
- package/dist/dev/infra-preflight.js +1 -1
- package/dist/dev/infra-preflight.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +10030 -1491
- package/dist/edition/verify-image-evidence.bundle.provenance.json +953 -419
- package/dist/runtime/runner/helm-cli.d.ts.map +1 -1
- package/dist/runtime/runner/helm-cli.js.map +1 -1
- package/dist/runtime/store-submit.d.ts.map +1 -1
- package/dist/runtime/store-submit.js.map +1 -1
- package/edition/README.md +50 -271
- package/edition/adapter-bundled-versions.mjs +107 -0
- package/edition/adapter-host-versions.mjs +41 -0
- package/edition/assemble-producer-input.mjs +1 -1
- package/edition/authorization-base.mjs +18 -9
- package/edition/biome-content.mjs +6 -0
- package/edition/build-platform-service-index.mjs +2 -0
- package/edition/build-runtime-image-source-index.mjs +97 -9
- package/edition/catalogue.mjs +20 -13
- package/edition/checkout-indexed-source-repositories.mjs +41 -6
- 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/dispatched-runtime-components.mjs +82 -0
- 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/external-source-walk.mjs +1 -1
- package/edition/image-evidence.mjs +134 -26
- package/edition/kernel-capability-domain.mjs +30 -0
- package/edition/kernel-package.mjs +193 -0
- package/edition/lib.mjs +192 -4
- package/edition/oci-evidence-verbs.mjs +2 -2
- package/edition/operation-grant-admission.mjs +70 -0
- package/edition/operation-requirements.mjs +128 -0
- package/edition/plan-image-evidence-refresh.mjs +2 -2
- package/edition/platform-lock-readability.mjs +2 -2
- package/edition/platform-service-class.mjs +42 -0
- package/edition/platform-service-ownership.mjs +78 -0
- 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/remedy-command.mjs +2 -2
- package/edition/resolve-chart-set.mjs +1 -1
- package/edition/resolve-customer-edition.mjs +33 -34
- package/edition/resolve-distribution.mjs +70 -16
- package/edition/resolve-edition.mjs +3 -2
- package/edition/resolve-images.mjs +65 -4
- package/edition/seal-image-digests.mjs +17 -5
- 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 +4 -4
|
@@ -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
|
+
}
|
|
@@ -38,7 +38,6 @@ import { assertKernelEditionProtocol } from './edition-protocol.mjs';
|
|
|
38
38
|
export const AUTHORIZATION_MODULE_DIRECTORY = 'authorization';
|
|
39
39
|
export const AUTHORIZATION_ENVELOPE_FILENAME = 'envelope.json';
|
|
40
40
|
export const AUTHORIZATION_LOCK_FILENAME = 'authorization.lock.json';
|
|
41
|
-
export const AUTHORIZATION_LOCK_SCHEMA_VERSION = 1;
|
|
42
41
|
|
|
43
42
|
/**
|
|
44
43
|
* The OpenFGA CLI the sealer tests with, EXACTLY. 0.8.1 embeds
|
|
@@ -142,10 +141,12 @@ export async function loadKernelAuthorizationComposer({ kernelPackageDir }) {
|
|
|
142
141
|
let server;
|
|
143
142
|
let contribution;
|
|
144
143
|
let generation;
|
|
144
|
+
let distribution;
|
|
145
145
|
try {
|
|
146
146
|
server = await import(`file://${serverDist}`);
|
|
147
147
|
contribution = await import(`file://${join(kernelPackageDir, 'dist', 'authorization-module', 'index.js')}`);
|
|
148
148
|
generation = await import(`file://${join(kernelPackageDir, 'dist', 'platform-generation', 'index.js')}`);
|
|
149
|
+
distribution = await import(`file://${join(kernelPackageDir, 'dist', 'distribution', 'index.js')}`);
|
|
149
150
|
} catch (err) {
|
|
150
151
|
throw new Error(`Cannot load the pinned kernel's authorization composer from ${serverDist}: ${err.message}`);
|
|
151
152
|
}
|
|
@@ -153,6 +154,7 @@ export async function loadKernelAuthorizationComposer({ kernelPackageDir }) {
|
|
|
153
154
|
[server, 'composeAuthorizationBase'],
|
|
154
155
|
[server, 'authorizationModuleInputFromContribution'],
|
|
155
156
|
[contribution, 'stampAuthorizationModuleContribution'],
|
|
157
|
+
[distribution, 'parseAuthorizationLockReadView'],
|
|
156
158
|
]) {
|
|
157
159
|
if (typeof mod[name] !== 'function') {
|
|
158
160
|
throw new Error(`the pinned kernel's authorization-module subpaths do not export ${name}.`);
|
|
@@ -169,6 +171,8 @@ export async function loadKernelAuthorizationComposer({ kernelPackageDir }) {
|
|
|
169
171
|
composeAuthorizationBase: server.composeAuthorizationBase,
|
|
170
172
|
authorizationModuleInputFromContribution: server.authorizationModuleInputFromContribution,
|
|
171
173
|
stampAuthorizationModuleContribution: contribution.stampAuthorizationModuleContribution,
|
|
174
|
+
parseAuthorizationLockReadView: distribution.parseAuthorizationLockReadView,
|
|
175
|
+
authorizationLockSchemaVersion: distribution.AUTHORIZATION_LOCK_SCHEMA_VERSION,
|
|
172
176
|
currentPlatformGeneration: parsed.data,
|
|
173
177
|
kernelPackageDir,
|
|
174
178
|
};
|
|
@@ -186,10 +190,11 @@ export function adoptedFromLock(lock) {
|
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
/** Compose an edition's base; a refusal names the edition and every violation. */
|
|
189
|
-
export function composeEditionAuthorizationBase({ distributionId, inputs, kernel, adopted }) {
|
|
193
|
+
export function composeEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, biomeOrigins }) {
|
|
190
194
|
try {
|
|
191
195
|
return kernel.composeAuthorizationBase({
|
|
192
196
|
modules: inputs,
|
|
197
|
+
biomeOrigins,
|
|
193
198
|
currentPlatformGeneration: kernel.currentPlatformGeneration,
|
|
194
199
|
...(adopted ? { adopted } : {}),
|
|
195
200
|
});
|
|
@@ -201,10 +206,10 @@ export function composeEditionAuthorizationBase({ distributionId, inputs, kernel
|
|
|
201
206
|
}
|
|
202
207
|
}
|
|
203
208
|
|
|
204
|
-
/** The lock body for a composed base. Deterministic: no timestamp. */
|
|
209
|
+
/** The lock body for a composed base. Deterministic: no timestamp. It is read through the kernel's `parseAuthorizationLockReadView` before it is returned. */
|
|
205
210
|
export function authorizationLockOf(distributionId, kernel, composed) {
|
|
206
|
-
|
|
207
|
-
schemaVersion:
|
|
211
|
+
const lock = {
|
|
212
|
+
schemaVersion: kernel.authorizationLockSchemaVersion,
|
|
208
213
|
distributionId,
|
|
209
214
|
platformGeneration: kernel.currentPlatformGeneration,
|
|
210
215
|
modelHash: composed.modelHash,
|
|
@@ -219,6 +224,8 @@ export function authorizationLockOf(distributionId, kernel, composed) {
|
|
|
219
224
|
parentImplementers: [...composed.parentImplementers],
|
|
220
225
|
model: composed.model,
|
|
221
226
|
};
|
|
227
|
+
kernel.parseAuthorizationLockReadView(lock);
|
|
228
|
+
return lock;
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
/** Resolve and verify the pinned `fga` CLI. */
|
|
@@ -272,8 +279,8 @@ export function fgaComposedModelTestRunner(bin) {
|
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
/** Compose, test, describe. `runTests` is the `fga model test` runner. */
|
|
275
|
-
export function sealEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, runTests }) {
|
|
276
|
-
const composed = composeEditionAuthorizationBase({ distributionId, inputs, kernel, adopted });
|
|
282
|
+
export function sealEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, biomeOrigins, runTests }) {
|
|
283
|
+
const composed = composeEditionAuthorizationBase({ distributionId, inputs, kernel, adopted, biomeOrigins });
|
|
277
284
|
runTests(composed);
|
|
278
285
|
return authorizationLockOf(distributionId, kernel, composed);
|
|
279
286
|
}
|
|
@@ -293,7 +300,7 @@ export const AuthorizationLockCheck = Object.freeze({
|
|
|
293
300
|
* exactly as the sealer would refuse it) and compares the whole lock. The
|
|
294
301
|
* tests are not re-run: an equal `modelHash` is the model the sealer tested.
|
|
295
302
|
*/
|
|
296
|
-
export function checkAuthorizationLockCurrent({ distributionId, committed, inputs, kernel, serialize }) {
|
|
303
|
+
export function checkAuthorizationLockCurrent({ distributionId, committed, inputs, kernel, biomeOrigins, serialize }) {
|
|
297
304
|
if (kernel === null) {
|
|
298
305
|
return committed === null ? AuthorizationLockCheck.GREEN : AuthorizationLockCheck.NOT_SEALABLE;
|
|
299
306
|
}
|
|
@@ -302,6 +309,7 @@ export function checkAuthorizationLockCurrent({ distributionId, committed, input
|
|
|
302
309
|
distributionId,
|
|
303
310
|
inputs,
|
|
304
311
|
kernel,
|
|
312
|
+
biomeOrigins,
|
|
305
313
|
adopted: adoptedFromLock(committed),
|
|
306
314
|
});
|
|
307
315
|
return serialize(authorizationLockOf(distributionId, kernel, composed)) === serialize(committed)
|
|
@@ -316,7 +324,7 @@ export function checkAuthorizationLockCurrent({ distributionId, committed, input
|
|
|
316
324
|
* entries plus the edition's source-walked ones). Returns an
|
|
317
325
|
* `AuthorizationLockCheck` member; throws on a refusal.
|
|
318
326
|
*/
|
|
319
|
-
export function checkEditionAuthorizationLock({ distributionId, editionDir, lockBiomes, index, kernel, serialize }) {
|
|
327
|
+
export function checkEditionAuthorizationLock({ distributionId, editionDir, lockBiomes, biomeOrigins, index, kernel, serialize }) {
|
|
320
328
|
if (!Array.isArray(index)) {
|
|
321
329
|
throw new Error(`${distributionId}: the authorization check needs the index the lock was resolved from.`);
|
|
322
330
|
}
|
|
@@ -325,6 +333,7 @@ export function checkEditionAuthorizationLock({ distributionId, editionDir, lock
|
|
|
325
333
|
committed: readAuthorizationLock(editionDir),
|
|
326
334
|
inputs: editionAuthorizationModuleInputs(lockBiomes, new Map(index.map((e) => [e.id, e])), kernel),
|
|
327
335
|
kernel,
|
|
336
|
+
biomeOrigins,
|
|
328
337
|
serialize,
|
|
329
338
|
});
|
|
330
339
|
}
|
|
@@ -20,6 +20,10 @@ import { execFileSync } from 'node:child_process';
|
|
|
20
20
|
import { relative, sep } from 'node:path';
|
|
21
21
|
|
|
22
22
|
import { readGitJsonAtCommit } from './lib.mjs';
|
|
23
|
+
import {
|
|
24
|
+
assertOperationGrantsAdmitted,
|
|
25
|
+
assertOrganizationSweepsAdmitted,
|
|
26
|
+
} from './operation-grant-admission.mjs';
|
|
23
27
|
|
|
24
28
|
/** A biome whose recomputed content hash is not the lock's, or whose inputs cannot be recomputed. */
|
|
25
29
|
export class BiomeContentHashError extends Error {
|
|
@@ -246,6 +250,8 @@ export async function biomeSourceTreeIds({ schemas, lock, imageLock, locate, ven
|
|
|
246
250
|
/** Seal: the unsealed lock + the recomputed hashes -> the kernel's `sealDistributionLock`. */
|
|
247
251
|
export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest }) {
|
|
248
252
|
const hashes = await computeBiomeContentHashes({ schemas, lock: unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest });
|
|
253
|
+
assertOperationGrantsAdmitted(schemas, unsealed);
|
|
254
|
+
assertOrganizationSweepsAdmitted(schemas, unsealed);
|
|
249
255
|
return schemas.sealDistributionLock(unsealed, hashes);
|
|
250
256
|
}
|
|
251
257
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
comparablePlatformService,
|
|
16
16
|
declaredInstallationClass,
|
|
17
|
+
sourceOwnedCapabilityDomains,
|
|
17
18
|
} from './platform-service-class.mjs';
|
|
18
19
|
import {
|
|
19
20
|
platformServiceSourceDeclaration,
|
|
@@ -142,6 +143,7 @@ function buildTrustedIndex(sourceAuthority) {
|
|
|
142
143
|
name: declaration.name,
|
|
143
144
|
tier: declaration.tier,
|
|
144
145
|
...declaredInstallationClass(declaration),
|
|
146
|
+
...sourceOwnedCapabilityDomains(sourceManifest, `${binding.path} at ${before}`),
|
|
145
147
|
repositoryUrl,
|
|
146
148
|
repo: binding.repo,
|
|
147
149
|
path: binding.path,
|
|
@@ -204,6 +204,10 @@ function assertVendoredSourcesResolve(crossRepoBuildInputs, resolveIdentity) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
const LEGACY_INDEX_SCHEMA_VERSION = 1;
|
|
208
|
+
const CURRENT_INDEX_SCHEMA_VERSION = 2;
|
|
209
|
+
const KNOWN_INDEX_SCHEMA_VERSIONS = Object.freeze([LEGACY_INDEX_SCHEMA_VERSION, CURRENT_INDEX_SCHEMA_VERSION]);
|
|
210
|
+
|
|
207
211
|
/**
|
|
208
212
|
* The kernel-contract authority for a pin regeneration.
|
|
209
213
|
*
|
|
@@ -250,13 +254,24 @@ function assertVendoredSourcesResolve(crossRepoBuildInputs, resolveIdentity) {
|
|
|
250
254
|
* rule — the caller states the commit (`--kernel-commit`) and the checkout's
|
|
251
255
|
* HEAD must equal it. Stating a commit when a committed authority exists is
|
|
252
256
|
* refused: it would be read by nobody.
|
|
257
|
+
*
|
|
258
|
+
* A committed index of schemaVersion 1 carries an authority a current kernel
|
|
259
|
+
* cannot validate, so it counts as having NO committed authority: it is
|
|
260
|
+
* migrated by bootstrap, which writes a schemaVersion 2 index. Any other
|
|
261
|
+
* schemaVersion is refused by name, never read as one of these.
|
|
253
262
|
*/
|
|
254
263
|
async function resolveContractAuthority(schemas, kernelRoot, kernelCommit) {
|
|
255
264
|
const repositoryDirectory = kernelRoot ?? undefined;
|
|
256
265
|
const committed = existsSync(runtimeImageSourceIndexPath())
|
|
257
266
|
? readJson(runtimeImageSourceIndexPath())
|
|
258
267
|
: undefined;
|
|
259
|
-
if (!committed
|
|
268
|
+
if (committed !== undefined && !KNOWN_INDEX_SCHEMA_VERSIONS.includes(committed.schemaVersion)) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`${runtimeImageSourceIndexPath()} has schemaVersion ${JSON.stringify(committed.schemaVersion)}; ` +
|
|
271
|
+
`this command reads ${KNOWN_INDEX_SCHEMA_VERSIONS.join(' or ')}`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (committed === undefined || committed.schemaVersion === LEGACY_INDEX_SCHEMA_VERSION || !committed.contractAuthority) {
|
|
260
275
|
return buildTrustedKernelContractAuthority({
|
|
261
276
|
...(repositoryDirectory === undefined ? {} : { repositoryDirectory }),
|
|
262
277
|
schemas,
|
|
@@ -360,6 +375,72 @@ function committedRepositoryPinsToPreserve(repoScope) {
|
|
|
360
375
|
);
|
|
361
376
|
}
|
|
362
377
|
|
|
378
|
+
/**
|
|
379
|
+
* `producerWorkflowPins` are operator-stated TRUST DATA: which commit of a
|
|
380
|
+
* producer repository's workflow the verifier believes. Nothing is derived (not
|
|
381
|
+
* from `repositoryPins`, not from a policy row); the committed pins are kept
|
|
382
|
+
* exactly, and `--producer-workflow-pin <owner/repo>=<40-hex>` sets or updates
|
|
383
|
+
* one. A bootstrap index with none committed writes `[]`, which the kernel allows.
|
|
384
|
+
*/
|
|
385
|
+
export function mergeProducerWorkflowPins(committedPins, statedPins) {
|
|
386
|
+
const merged = new Map(
|
|
387
|
+
(Array.isArray(committedPins) ? committedPins : []).map((pin) => [
|
|
388
|
+
pin.repository,
|
|
389
|
+
{ repository: pin.repository, workflowCommit: pin.workflowCommit },
|
|
390
|
+
]),
|
|
391
|
+
);
|
|
392
|
+
for (const pin of statedPins) {
|
|
393
|
+
merged.set(pin.repository, { repository: pin.repository, workflowCommit: pin.workflowCommit });
|
|
394
|
+
}
|
|
395
|
+
return [...merged.values()].sort((left, right) => compareStrings(left.repository, right.repository));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const PRODUCER_WORKFLOW_PIN_FLAG = '--producer-workflow-pin';
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Every `--producer-workflow-pin <owner/repo>=<40-hex>` of the argv. A malformed
|
|
402
|
+
* value, or a repository stated twice, is refused here; the repository and
|
|
403
|
+
* commit grammar is the kernel's, applied by the index parse.
|
|
404
|
+
*/
|
|
405
|
+
export function parseProducerWorkflowPinFlags(argv) {
|
|
406
|
+
const pins = [];
|
|
407
|
+
const remaining = [];
|
|
408
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
409
|
+
if (argv[index] !== PRODUCER_WORKFLOW_PIN_FLAG) {
|
|
410
|
+
remaining.push(argv[index]);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const value = argv[index + 1];
|
|
414
|
+
const separator = value?.indexOf('=') ?? -1;
|
|
415
|
+
if (separator < 1 || value.startsWith('--')) {
|
|
416
|
+
throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} requires <owner/repo>=<40-hex-commit>`);
|
|
417
|
+
}
|
|
418
|
+
const repository = value.slice(0, separator);
|
|
419
|
+
const workflowCommit = value.slice(separator + 1);
|
|
420
|
+
if (pins.some((pin) => pin.repository === repository)) {
|
|
421
|
+
throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} names ${repository} more than once`);
|
|
422
|
+
}
|
|
423
|
+
if (!COMMIT_PATTERN.test(workflowCommit)) {
|
|
424
|
+
throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} ${repository}: the commit must be a full 40-hex sha`);
|
|
425
|
+
}
|
|
426
|
+
pins.push({ repository, workflowCommit });
|
|
427
|
+
index += 1;
|
|
428
|
+
}
|
|
429
|
+
return { producerWorkflowPins: pins, remaining };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* A `--producer-workflow-pin` is trust data for the index `--trusted-write`
|
|
434
|
+
* writes, and only there. Every other mode writes nothing, so a pin stated to
|
|
435
|
+
* one would be silently ignored; `--check-sources` included: it grades the
|
|
436
|
+
* committed index against the sources, and a pin is not a source.
|
|
437
|
+
*/
|
|
438
|
+
export function assertProducerWorkflowPinsWritten(producerWorkflowPins, flag, usage) {
|
|
439
|
+
if (producerWorkflowPins.length > 0 && flag !== '--trusted-write') {
|
|
440
|
+
throw new Error(`${PRODUCER_WORKFLOW_PIN_FLAG} states trust data for a written index: ${flag} writes none\n${usage}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
363
444
|
/**
|
|
364
445
|
* `biomeIndex` and `platformServiceIndex` are the indexes this one is built
|
|
365
446
|
* over, INJECTED: the release CLI passes the committed ones read from disk
|
|
@@ -371,7 +452,7 @@ export async function buildTrustedIndex(
|
|
|
371
452
|
sourceAuthority,
|
|
372
453
|
kernelRoot,
|
|
373
454
|
repoScope,
|
|
374
|
-
{ biomeIndex, platformServiceIndex, kernelCommit },
|
|
455
|
+
{ biomeIndex, platformServiceIndex, kernelCommit, producerWorkflowPins = [] },
|
|
375
456
|
) {
|
|
376
457
|
const contractAuthority = await resolveContractAuthority(schemas, kernelRoot, kernelCommit);
|
|
377
458
|
const repositoryIdentities = new Map();
|
|
@@ -421,7 +502,7 @@ export async function buildTrustedIndex(
|
|
|
421
502
|
const repositoryPins = [...sourceRepos]
|
|
422
503
|
.sort(compareStrings)
|
|
423
504
|
.map(getRepositoryIdentity);
|
|
424
|
-
const
|
|
505
|
+
const sortedCrossRepoBuildInputs = [...crossRepoBuildInputs()].sort(
|
|
425
506
|
(left, right) =>
|
|
426
507
|
compareStrings(
|
|
427
508
|
[left.consumerRepo, left.consumerService, left.destinationPath].join(
|
|
@@ -432,7 +513,7 @@ export async function buildTrustedIndex(
|
|
|
432
513
|
),
|
|
433
514
|
),
|
|
434
515
|
);
|
|
435
|
-
assertVendoredSourcesResolve(
|
|
516
|
+
assertVendoredSourcesResolve(sortedCrossRepoBuildInputs, getRepositoryIdentity);
|
|
436
517
|
const buildStepContextDirectories = [...buildStepContextDirectoriesDeclared()].sort(
|
|
437
518
|
(left, right) =>
|
|
438
519
|
compareStrings(
|
|
@@ -448,7 +529,7 @@ export async function buildTrustedIndex(
|
|
|
448
529
|
);
|
|
449
530
|
}
|
|
450
531
|
}
|
|
451
|
-
for (const input of
|
|
532
|
+
for (const input of sortedCrossRepoBuildInputs) {
|
|
452
533
|
for (const repo of [input.consumerRepo, input.sourceRepo]) {
|
|
453
534
|
if (!sourceRepos.has(repo)) {
|
|
454
535
|
throw new Error(
|
|
@@ -459,12 +540,16 @@ export async function buildTrustedIndex(
|
|
|
459
540
|
}
|
|
460
541
|
}
|
|
461
542
|
return {
|
|
462
|
-
schemaVersion:
|
|
543
|
+
schemaVersion: CURRENT_INDEX_SCHEMA_VERSION,
|
|
463
544
|
contractAuthority,
|
|
464
545
|
supplementalSubjects,
|
|
465
|
-
crossRepoBuildInputs,
|
|
546
|
+
crossRepoBuildInputs: sortedCrossRepoBuildInputs,
|
|
466
547
|
buildStepContextDirectories,
|
|
467
548
|
repositoryPins,
|
|
549
|
+
producerWorkflowPins: mergeProducerWorkflowPins(
|
|
550
|
+
existsSync(runtimeImageSourceIndexPath()) ? readJson(runtimeImageSourceIndexPath()).producerWorkflowPins : [],
|
|
551
|
+
producerWorkflowPins,
|
|
552
|
+
),
|
|
468
553
|
platformServiceBindings: [...platformServiceRuntimeBindings()].sort(
|
|
469
554
|
(left, right) =>
|
|
470
555
|
compareStrings(left.platformService, right.platformService),
|
|
@@ -749,7 +834,7 @@ const USAGE =
|
|
|
749
834
|
'usage: build-runtime-image-source-index.mjs (--check | --check-sources | --trusted-write | ' +
|
|
750
835
|
'--repin-contract-authority --kernel-commit <40-hex-commit>) ' +
|
|
751
836
|
'[--source published|workspace] [--kernel-root <absolute-checkout>] ' +
|
|
752
|
-
'[--repo <repository> [--at <40-hex-commit>]] ' +
|
|
837
|
+
'[--repo <repository> [--at <40-hex-commit>]] [--producer-workflow-pin <owner/repo>=<40-hex-commit> ...] ' +
|
|
753
838
|
'(--trusted-write on a data root with no committed contract authority also requires --kernel-commit)';
|
|
754
839
|
|
|
755
840
|
async function main() {
|
|
@@ -757,7 +842,8 @@ async function main() {
|
|
|
757
842
|
process.argv.slice(2),
|
|
758
843
|
);
|
|
759
844
|
const { kernelCommit, remaining: afterKernelRoot } = parseKernelCommit(afterKernelRootFlag);
|
|
760
|
-
const {
|
|
845
|
+
const { producerWorkflowPins, remaining: afterWorkflowPins } = parseProducerWorkflowPinFlags(afterKernelRoot);
|
|
846
|
+
const { repoScope, remaining: afterRepoScope } = parseRepoScope(afterWorkflowPins);
|
|
761
847
|
const { exactCommit, remaining: argv } = parseExactCommit(afterRepoScope);
|
|
762
848
|
const { mode, remaining } = parseSourceModeArgs(argv);
|
|
763
849
|
if (exactCommit !== undefined && repoScope === undefined) {
|
|
@@ -773,6 +859,7 @@ async function main() {
|
|
|
773
859
|
if (kernelCommit !== undefined && ![REPIN_FLAG, '--trusted-write'].includes(flag)) {
|
|
774
860
|
throw new Error(`${KERNEL_COMMIT_FLAG} states the commit of a new contract authority: ${flag} chooses none\n${USAGE}`);
|
|
775
861
|
}
|
|
862
|
+
assertProducerWorkflowPinsWritten(producerWorkflowPins, flag, USAGE);
|
|
776
863
|
const schemas = await loadKernelDistributionSchemas();
|
|
777
864
|
if (flag === REPIN_FLAG) {
|
|
778
865
|
if (repoScope !== undefined || exactCommit !== undefined) {
|
|
@@ -828,6 +915,7 @@ async function main() {
|
|
|
828
915
|
biomeIndex: readJson(biomeIndexPath()),
|
|
829
916
|
platformServiceIndex: readJson(join(dataRoot(), 'platform-service-index.json')),
|
|
830
917
|
kernelCommit,
|
|
918
|
+
producerWorkflowPins,
|
|
831
919
|
},
|
|
832
920
|
),
|
|
833
921
|
);
|
package/edition/catalogue.mjs
CHANGED
|
@@ -14,13 +14,18 @@ import { join } from 'node:path';
|
|
|
14
14
|
import { assertCanonicalDistributionId } from './distribution-paths.mjs';
|
|
15
15
|
import { EDITION_OFFERING_LAYERS, EditionChannel } from './edition-constants.mjs';
|
|
16
16
|
import { serializeJson } from './lib.mjs';
|
|
17
|
+
import { verifiedPlatformServiceIndex } from './platform-service-ownership.mjs';
|
|
17
18
|
|
|
18
19
|
export const CATALOGUE_FILE = EDITION_OFFERING_LAYERS.catalogue;
|
|
19
20
|
export const CATALOGUE_SCHEMA = 'xema.edition-catalogue/v1';
|
|
20
21
|
|
|
21
|
-
const TOP_KEYS = ['schema', 'offeringId', 'channel', 'biomes', 'sourceTreeIds', 'platformServices', 'images', 'chartSet', 'contractAuthority', 'ceiling'];
|
|
22
|
+
const TOP_KEYS = ['schema', 'offeringId', 'channel', 'biomes', 'sourceTreeIds', 'platformServices', 'images', 'chartSet', 'contractAuthority', 'kernel', 'ceiling'];
|
|
22
23
|
const IMAGES_KEYS = ['sourceHashAlgorithm', 'subjects', 'platformServiceBindings', 'webBiomes'];
|
|
23
24
|
const CEILING_KEYS = ['trustPolicy', 'trustCeiling'];
|
|
25
|
+
const KERNEL_KEYS = ['name', 'version', 'integrity'];
|
|
26
|
+
const KERNEL_PACKAGE = '@xemahq/contracts';
|
|
27
|
+
const EXACT_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
28
|
+
const NPM_INTEGRITY = /^sha512-[A-Za-z0-9+/]{86}==$/;
|
|
24
29
|
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
25
30
|
const GIT_TREE_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
26
31
|
|
|
@@ -76,6 +81,14 @@ export function parseCatalogue(value) {
|
|
|
76
81
|
}
|
|
77
82
|
if (!isObject(value.chartSet)) {throw new CatalogueError(`${CATALOGUE_FILE}: chartSet must be the offering's chart-set lock`);}
|
|
78
83
|
if (!isObject(value.contractAuthority)) {throw new CatalogueError(`${CATALOGUE_FILE}: contractAuthority must be the runtime image source index's authority`);}
|
|
84
|
+
exactKeys(value.kernel, KERNEL_KEYS, `${CATALOGUE_FILE} kernel`);
|
|
85
|
+
if (value.kernel.name !== KERNEL_PACKAGE) {throw new CatalogueError(`${CATALOGUE_FILE}: kernel.name must be ${KERNEL_PACKAGE}, not ${JSON.stringify(value.kernel.name)}`);}
|
|
86
|
+
if (typeof value.kernel.version !== 'string' || !EXACT_SEMVER.test(value.kernel.version)) {
|
|
87
|
+
throw new CatalogueError(`${CATALOGUE_FILE}: kernel.version must be an exact x.y.z version, not ${JSON.stringify(value.kernel.version)}`);
|
|
88
|
+
}
|
|
89
|
+
if (typeof value.kernel.integrity !== 'string' || !NPM_INTEGRITY.test(value.kernel.integrity)) {
|
|
90
|
+
throw new CatalogueError(`${CATALOGUE_FILE}: kernel.integrity must be an npm sha512 integrity (sha512-<base64>)`);
|
|
91
|
+
}
|
|
79
92
|
exactKeys(value.ceiling, CEILING_KEYS, `${CATALOGUE_FILE} ceiling`);
|
|
80
93
|
return value;
|
|
81
94
|
}
|
|
@@ -94,8 +107,10 @@ export function buildCatalogue({
|
|
|
94
107
|
biomeIndex,
|
|
95
108
|
platformServiceIndex,
|
|
96
109
|
contractAuthority,
|
|
110
|
+
kernel,
|
|
97
111
|
leaf,
|
|
98
112
|
sourceTreeIds,
|
|
113
|
+
readManifest,
|
|
99
114
|
}) {
|
|
100
115
|
const sourced = distributionLock.biomes.filter((biome) => biome.source !== undefined);
|
|
101
116
|
if (sourced.length > 0) {
|
|
@@ -106,7 +121,9 @@ export function buildCatalogue({
|
|
|
106
121
|
const biomes = biomeIndex.filter((row) => shippedBiomes.has(row.id));
|
|
107
122
|
const missing = [...shippedBiomes].filter((id) => !biomes.some((row) => row.id === id));
|
|
108
123
|
if (missing.length > 0) {throw new CatalogueError(`offering "${offeringId}": the biome index has no row for ${missing.join(', ')}`);}
|
|
109
|
-
|
|
124
|
+
// The customer trusts this catalogue's ownership rows without sources, so they are
|
|
125
|
+
// re-derived from the pinned manifests here, before the catalogue seals.
|
|
126
|
+
const platformServices = verifiedPlatformServiceIndex(platformServiceIndex, { names: shippedServices, readManifest }).services.filter((row) => shippedServices.has(row.name));
|
|
110
127
|
return parseCatalogue({
|
|
111
128
|
schema: CATALOGUE_SCHEMA,
|
|
112
129
|
offeringId,
|
|
@@ -122,6 +139,7 @@ export function buildCatalogue({
|
|
|
122
139
|
},
|
|
123
140
|
chartSet: chartSetLock,
|
|
124
141
|
contractAuthority,
|
|
142
|
+
kernel,
|
|
125
143
|
ceiling: { trustPolicy: leaf?.trustPolicy ?? null, trustCeiling: leaf?.trustCeiling ?? null },
|
|
126
144
|
});
|
|
127
145
|
}
|
|
@@ -133,14 +151,3 @@ export function writeCatalogue(outDir, catalogue) {
|
|
|
133
151
|
writeFileSync(path, serializeJson(catalogue));
|
|
134
152
|
return path;
|
|
135
153
|
}
|
|
136
|
-
|
|
137
|
-
const KERNEL_COMMIT = /^[0-9a-f]{40}$/;
|
|
138
|
-
|
|
139
|
-
/** The commit the catalogue's contract authority was taken at (40 hex); read without the kernel schemas, which are not checked out yet where this is used. */
|
|
140
|
-
export function contractAuthoritySourceCommit(catalogue) {
|
|
141
|
-
const commit = catalogue.contractAuthority.sourceCommit;
|
|
142
|
-
if (typeof commit !== 'string' || !KERNEL_COMMIT.test(commit)) {
|
|
143
|
-
throw new CatalogueError(`${CATALOGUE_FILE}: contractAuthority.sourceCommit is not a 40-hex commit: ${JSON.stringify(commit)}`);
|
|
144
|
-
}
|
|
145
|
-
return commit;
|
|
146
|
-
}
|
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { dirname, resolve } from 'node:path';
|
|
6
6
|
|
|
7
7
|
import { editionWorkspaceConfig } from './edition-config.mjs';
|
|
8
|
-
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
|
|
8
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, platformServiceIndexPath, runtimeImageSourceIndexPath, sourcesRoot } from './edition-workspace.mjs';
|
|
9
9
|
import { holdSource, withSourceGitEnv } from './git-source-auth.mjs';
|
|
10
10
|
import { applyPinSource, snapshotPinSource } from './pin-source.mjs';
|
|
11
11
|
import { readStrictJsonFile } from './strict-json.mjs';
|
|
@@ -45,10 +45,40 @@ function git(args, env) {
|
|
|
45
45
|
execFileSync('git', args, { env, stdio: 'inherit' });
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The extra commits each checkout must also hold: resolve reads every platform
|
|
50
|
+
* service's manifest at the `sourceCommit` platform-service-index.json pins, and
|
|
51
|
+
* that commit may differ from the repository's own pin. Maps repo to the
|
|
52
|
+
* distinct such commits (excluding the pinned one). A platform service whose
|
|
53
|
+
* repository has no pin is refused: there is no checkout to fetch into.
|
|
54
|
+
*/
|
|
55
|
+
export function platformServiceExtraCommits(plan, platformServiceIndex) {
|
|
56
|
+
if (!Array.isArray(platformServiceIndex?.services)) {
|
|
57
|
+
throw new Error('platform-service-index.json declares no services array');
|
|
58
|
+
}
|
|
59
|
+
const pinned = new Map(plan.map((entry) => [entry.repo, entry.sourceCommit]));
|
|
60
|
+
const extra = new Map();
|
|
61
|
+
for (const service of platformServiceIndex.services) {
|
|
62
|
+
if (!pinned.has(service?.repo)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`platform service "${service?.name}" names repository ${JSON.stringify(service?.repo)}, which has no repository pin in the runtime image source index`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!SHA.test(String(service.sourceCommit ?? ''))) {
|
|
68
|
+
throw new Error(`platform service "${service.name}" sourceCommit is not exact lowercase 40-hex`);
|
|
69
|
+
}
|
|
70
|
+
if (service.sourceCommit === pinned.get(service.repo)) {continue;}
|
|
71
|
+
if (!extra.has(service.repo)) {extra.set(service.repo, new Set());}
|
|
72
|
+
extra.get(service.repo).add(service.sourceCommit);
|
|
73
|
+
}
|
|
74
|
+
return new Map([...extra].map(([repo, commits]) => [repo, [...commits]]));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function checkoutIndexedSources({ workspaceRoot, index, platformServiceIndex, env = process.env, runGit = git }) {
|
|
49
78
|
if (!workspaceRoot) {throw new Error('workspaceRoot is required');}
|
|
50
79
|
const workspace = editionWorkspaceConfig();
|
|
51
80
|
const plan = sourceCheckoutPlan(index, workspace);
|
|
81
|
+
const extraCommits = platformServiceExtraCommits(plan, platformServiceIndex);
|
|
52
82
|
for (const entry of plan) {holdSource({ name: entry.repo, url: entry.repositoryUrl }, workspace, env);}
|
|
53
83
|
for (const entry of plan) {
|
|
54
84
|
const target = resolve(workspaceRoot, entry.repo);
|
|
@@ -57,10 +87,10 @@ export function checkoutIndexedSources({ workspaceRoot, index, env = process.env
|
|
|
57
87
|
}
|
|
58
88
|
withSourceGitEnv({ name: entry.repo, url: entry.repositoryUrl }, { workspace, env }, (gitEnv) => {
|
|
59
89
|
mkdirSync(dirname(target), { recursive: true });
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
90
|
+
runGit(['init', '--quiet', target], gitEnv);
|
|
91
|
+
runGit(['-C', target, 'remote', 'add', 'origin', entry.repositoryUrl], gitEnv);
|
|
92
|
+
runGit(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', entry.sourceCommit], gitEnv);
|
|
93
|
+
runGit(['-C', target, 'checkout', '--quiet', '--detach', entry.sourceCommit], gitEnv);
|
|
64
94
|
const actual = execFileSync('git', ['-C', target, 'rev-parse', 'HEAD'], {
|
|
65
95
|
env: gitEnv,
|
|
66
96
|
encoding: 'utf8',
|
|
@@ -68,6 +98,10 @@ export function checkoutIndexedSources({ workspaceRoot, index, env = process.env
|
|
|
68
98
|
if (actual !== entry.sourceCommit) {
|
|
69
99
|
throw new Error(`${entry.repo} resolved ${actual}, expected ${entry.sourceCommit}`);
|
|
70
100
|
}
|
|
101
|
+
for (const commit of extraCommits.get(entry.repo) ?? []) {
|
|
102
|
+
runGit(['-C', target, 'fetch', '--quiet', '--depth=1', 'origin', commit], gitEnv);
|
|
103
|
+
console.log(`[source-checkout] ${entry.repo} also holds platform-service commit ${commit}`);
|
|
104
|
+
}
|
|
71
105
|
});
|
|
72
106
|
console.log(`[source-checkout] ${entry.repo} @ ${entry.sourceCommit}`);
|
|
73
107
|
}
|
|
@@ -96,6 +130,7 @@ if (isEntryModule(import.meta.url)) {
|
|
|
96
130
|
const count = checkoutIndexedSources({
|
|
97
131
|
workspaceRoot: sourcesRoot(),
|
|
98
132
|
index,
|
|
133
|
+
platformServiceIndex: JSON.parse(readFileSync(platformServiceIndexPath(), 'utf8')),
|
|
99
134
|
});
|
|
100
135
|
console.log(`[source-checkout] materialized ${count} indexed repositories at exact commits`);
|
|
101
136
|
} catch (error) {
|
|
@@ -57,32 +57,15 @@ export function parseCustomerDefinition(schemas, raw) {
|
|
|
57
57
|
} catch (error) {
|
|
58
58
|
throw new CustomerDefinitionError(error.message, { cause: error });
|
|
59
59
|
}
|
|
60
|
-
assertNoVendorReservedDomain(schemas, definition);
|
|
61
60
|
return { definition, mirror };
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
/** The trust domains only the vendor places a biome in: the kernel's first-party domain. */
|
|
65
|
-
export const vendorReservedTrustDomains = (schemas) => [schemas.FIRST_PARTY_TRUST_DOMAIN];
|
|
66
|
-
|
|
67
|
-
/** A customer's `trustCeiling` assigns its own biomes and never into a vendor-reserved domain. */
|
|
68
|
-
function assertNoVendorReservedDomain(schemas, definition) {
|
|
69
|
-
const reserved = new Set(vendorReservedTrustDomains(schemas));
|
|
70
|
-
for (const assignment of definition.trustCeiling?.assignments ?? []) {
|
|
71
|
-
if (reserved.has(assignment.domain)) {
|
|
72
|
-
throw new CustomerDefinitionError(
|
|
73
|
-
`customer definition "${definition.id}": \`trustCeiling\` assigns ${assignment.biomes.map((biome) => `biome "${biome}"`).join(', ')} ` +
|
|
74
|
-
`to trust domain "${assignment.domain}", which is reserved to the vendor; a customer's biomes are assigned a domain of the customer's own`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
63
|
/**
|
|
81
64
|
* The trustCeiling must assign every customer-source biome (a biome the resolved lock draws
|
|
82
|
-
* from the definition's own `sources[]`) and no other: the kernel's `customerTrustCeilingIssues`.
|
|
65
|
+
* from the definition's own `sources[]`) and no other, and none in a domain the catalogue's ceiling reserves: the kernel's `customerTrustCeilingIssues`.
|
|
83
66
|
*/
|
|
84
|
-
export function assertTrustCeilingAssignsCustomerBiomes(schemas, definition, customerBiomeIds) {
|
|
85
|
-
const issues = schemas.customerTrustCeilingIssues(definition, customerBiomeIds);
|
|
67
|
+
export function assertTrustCeilingAssignsCustomerBiomes(schemas, definition, customerBiomeIds, catalogueTrustCeiling) {
|
|
68
|
+
const issues = schemas.customerTrustCeilingIssues(definition, customerBiomeIds, catalogueTrustCeiling);
|
|
86
69
|
if (issues.length > 0) {
|
|
87
70
|
throw new CustomerDefinitionError(`customer definition "${definition.id}": ${issues.join('; ')}`);
|
|
88
71
|
}
|
|
@@ -72,7 +72,7 @@ export function declaredSourceNamesOf(rawDefinition) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** Resolve and seal the develop customer edition; returns the four locks and the frozen provenance pins. */
|
|
75
|
-
export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue, catalogueDigest, pins, sourceImages, env = process.env, sealOptions = {},
|
|
75
|
+
export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue, catalogueDigest, pins, sourceImages, env = process.env, sealOptions = {}, adoptKernel, materialize }) {
|
|
76
76
|
const frozen = customerDevelopPins({ pins, declaredSourceNames: declaredSourceNamesOf(rawDefinition), catalogueDigest });
|
|
77
77
|
const resolved = await resolveCustomerEdition({
|
|
78
78
|
rawDefinition,
|
|
@@ -81,7 +81,7 @@ export async function resolveCustomerDevelopEdition({ rawDefinition, catalogue,
|
|
|
81
81
|
sourceImages,
|
|
82
82
|
pins: frozen,
|
|
83
83
|
catalogueDigest,
|
|
84
|
-
...(
|
|
84
|
+
...(adoptKernel === undefined ? {} : { adoptKernel }),
|
|
85
85
|
...(materialize === undefined ? {} : { materialize }),
|
|
86
86
|
sealOwn: ({ distributionId, subjects }) => sealDevelopImageSubjects({ distributionId, subjects, env, ...sealOptions }),
|
|
87
87
|
});
|