@xemahq/xema 0.41.1 → 0.42.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/commands/edition.js +1 -1
- package/dist/commands/edition.js.map +1 -1
- package/dist/edition/verify-image-evidence.bundle.mjs +1289 -840
- package/dist/edition/verify-image-evidence.bundle.provenance.json +494 -458
- package/edition/README.md +3 -1
- package/edition/biome-content.mjs +4 -0
- package/edition/customer-edition-verbs.mjs +1 -0
- package/edition/develop-edition.mjs +22 -2
- package/edition/dispatched-runtime-components.mjs +4 -3
- package/edition/edition-protocol.mjs +36 -9
- package/edition/edition-workspace.mjs +2 -0
- package/edition/kernel-protocol-choice.mjs +239 -0
- package/edition/lib.mjs +100 -3
- package/edition/operation-grant-admission.mjs +65 -1
- package/edition/operation-requirements.mjs +18 -1
- package/edition/resolve-customer-edition.mjs +4 -3
- package/edition/resolve-distribution.mjs +16 -0
- package/edition/resolve-images.mjs +124 -15
- package/package.json +4 -4
package/edition/README.md
CHANGED
|
@@ -100,7 +100,9 @@ The edition library builds, validates and verifies evidence statements; it never
|
|
|
100
100
|
|
|
101
101
|
## Kernel protocol and pins
|
|
102
102
|
|
|
103
|
-
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
|
|
103
|
+
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 v9 to v14, each pinned to its digest and vendored (v10 to v14 grow the kernel source closure only; a data root whose pinned kernel speaks v8 stays on CLI 0.41.x, v5 on 0.33.x) 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.
|
|
104
|
+
|
|
105
|
+
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`.
|
|
104
106
|
|
|
105
107
|
- `xema edition evidence statement entitlement stage` writes entitlement predicate v2 (`lockDigest`, `catalogueDigest`, read from the edition's committed locks), so it takes `--editions-root` besides `--data-root` and `--kernel`.
|
|
106
108
|
- A source repository JOINS a data root that has committed pins with one command: `xema edition index runtime-image-source --trusted-write --repo <repository> [--at <40-hex>] [--kernel-root <checkout>]`. The repository is resolved once and the runtime image source, biome and platform-service indexes are all written from the committed pins plus that one pin; every committed pin is kept field for field (a differing one is refused by name), a second declared repository with no pin is refused, and the summary names each pin added, moved and dropped. Then `xema edition index biome --check`, `xema edition index runtime-image-source --check` and `pnpm check`. The biome builder takes no `--repo`: it would resolve the repository a second time.
|
|
@@ -22,8 +22,10 @@ import { relative, sep } from 'node:path';
|
|
|
22
22
|
import { assertCustomerLockWithinCatalogueCeiling } from './catalogue.mjs';
|
|
23
23
|
import { readGitJsonAtCommit } from './lib.mjs';
|
|
24
24
|
import {
|
|
25
|
+
assertInstalledOperationCeilingAdmitted,
|
|
25
26
|
assertOperationGrantsAdmitted,
|
|
26
27
|
assertOrganizationSweepsAdmitted,
|
|
28
|
+
assertStoreWorkloadCeilingAdmitted,
|
|
27
29
|
} from './operation-grant-admission.mjs';
|
|
28
30
|
|
|
29
31
|
/** A biome whose recomputed content hash is not the lock's, or whose inputs cannot be recomputed. */
|
|
@@ -289,6 +291,8 @@ export async function sealLock({ schemas, unsealed, imageLock, locate, vendoredC
|
|
|
289
291
|
const hashes = await computeBiomeContentHashes({ schemas, lock: unsealed, imageLock, locate, vendoredCommit, catalogue, catalogueDigest, storePublisherAnchorKeys, sourceFactsById });
|
|
290
292
|
assertOperationGrantsAdmitted(schemas, unsealed);
|
|
291
293
|
assertOrganizationSweepsAdmitted(schemas, unsealed);
|
|
294
|
+
assertInstalledOperationCeilingAdmitted(schemas, unsealed);
|
|
295
|
+
assertStoreWorkloadCeilingAdmitted(schemas, unsealed);
|
|
292
296
|
return schemas.sealDistributionLock(unsealed, hashes);
|
|
293
297
|
}
|
|
294
298
|
|
|
@@ -214,6 +214,7 @@ const COMMANDS = Object.freeze({
|
|
|
214
214
|
required: ['--pins', '--cosign'],
|
|
215
215
|
},
|
|
216
216
|
'check-overrides': { positional: false, values: ['--overrides'], switches: [], required: ['--overrides'] },
|
|
217
|
+
'choose-kernel': { positional: false, values: ['--kernel-pin', '--out'], switches: [], required: ['--out'] },
|
|
217
218
|
'check-existing': {
|
|
218
219
|
positional: true,
|
|
219
220
|
values: ['--pins', '--artifact', '--engine-image'],
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
// [--overrides <json>] --out <file>
|
|
31
31
|
// resolve refs/heads/develop once per graph repository (and the editions
|
|
32
32
|
// root) from this checkout's runtime-image-source-index.json; the three checked-out
|
|
33
|
-
// repositories are pinned at what the workflow checked out; write the pins.
|
|
33
|
+
// repositories are pinned at what the workflow checked out; write the pins. --kernel-sha is refused
|
|
34
|
+
// unless its declared edition protocol is one this CLI supports (the check choose-kernel applies to a pin).
|
|
34
35
|
// check-existing <id> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
35
36
|
// refuse to reuse a pulled artifact whose descriptor disagrees with this snapshot.
|
|
36
37
|
// check-existing --definition <file> --pins <file> --artifact <dir> --engine-image <repository>@sha256:<digest>
|
|
@@ -44,6 +45,13 @@
|
|
|
44
45
|
// annotations-missing | annotation-mismatch | not-built). Prints a table grouped by producer repo
|
|
45
46
|
// and `covered N/M` (--json: the same as data). Exit 0 only when every subject is covered, 1 when
|
|
46
47
|
// not, 2 on an operational error. Takes the roots `resolve` takes; writes nothing.
|
|
48
|
+
// choose-kernel [--kernel-pin <40-hex>] --out <file>
|
|
49
|
+
// choose the develop kernel commit in the --kernel checkout (full history; its refs/remotes/origin/develop is the develop head):
|
|
50
|
+
// the newest first-parent commit whose declared edition protocol this CLI supports
|
|
51
|
+
// (kernel-protocol-choice.mjs). Writes {chosenSha, headSha, held, reason} (reason: head-supported |
|
|
52
|
+
// head-ahead | explicit-pin); when held, prints `kernel held at v<N> (<sha12>), head at v<M> (<sha12>):
|
|
53
|
+
// raise the CLI` to stdout (never persisted). A pin wins and is still checked; nothing supported is refused.
|
|
54
|
+
// Takes only --kernel (the checkout need not be built).
|
|
47
55
|
// check-overrides --overrides <json>
|
|
48
56
|
// refuse a pins override that would select the tooling this run executes
|
|
49
57
|
// (the edition tooling's own repository); the workflow runs it before any other checkout.
|
|
@@ -68,8 +76,9 @@ import { resolveDevelopEdition } from './develop-edition-resolve.mjs';
|
|
|
68
76
|
import { assertNoToolingOverride, freezeGraph, freezeSources } from './develop-freeze.mjs';
|
|
69
77
|
import { LOCAL_WORKFLOW, assertExistingArtifactMatches, developEditionTarget, workflowFromEnv, writeEditionArtifact } from './edition-artifact.mjs';
|
|
70
78
|
import { EditionChannel } from './edition-constants.mjs';
|
|
71
|
-
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
79
|
+
import { WorkspaceRoot, configureEditionWorkspaceFromProcess, isEntryModule, kernelRepoDir, runtimeImageSourceIndexPath } from './edition-workspace.mjs';
|
|
72
80
|
import { descriptorEvidenceTrust } from './evidence-trust-policy.mjs';
|
|
81
|
+
import { assertKernelCommitSupported, chooseKernelCommit } from './kernel-protocol-choice.mjs';
|
|
73
82
|
import { loadKernelDistributionSchemas } from './lib.mjs';
|
|
74
83
|
import { snapshotPinSource } from './pin-source.mjs';
|
|
75
84
|
import { composeEdition } from './resolve-distribution.mjs';
|
|
@@ -85,11 +94,15 @@ const VENDOR_ROOTS = Object.freeze([WorkspaceRoot.Data, WorkspaceRoot.Editions,
|
|
|
85
94
|
const COVERAGE_COMMAND = 'coverage';
|
|
86
95
|
const COVERAGE_OPERATIONAL_EXIT = CoverageExit.Operational;
|
|
87
96
|
const DATA_ROOT_ONLY_COMMANDS = Object.freeze(['check-overrides']);
|
|
97
|
+
// `choose-kernel` reads the kernel checkout's git history only, before the kernel is built.
|
|
98
|
+
const KERNEL_ROOT_ONLY_COMMANDS = Object.freeze(['choose-kernel']);
|
|
88
99
|
|
|
89
100
|
if (isEntryModule(import.meta.url)) {
|
|
90
101
|
const [command] = process.argv.slice(2);
|
|
91
102
|
if (DATA_ROOT_ONLY_COMMANDS.includes(command)) {
|
|
92
103
|
configureEditionWorkspaceFromProcess([WorkspaceRoot.Data], { refuseOthers: true });
|
|
104
|
+
} else if (KERNEL_ROOT_ONLY_COMMANDS.includes(command)) {
|
|
105
|
+
configureEditionWorkspaceFromProcess([WorkspaceRoot.Kernel], { refuseOthers: true });
|
|
93
106
|
} else {
|
|
94
107
|
const customerForm = process.argv.includes(DEFINITION_FLAG);
|
|
95
108
|
configureEditionWorkspaceFromProcess(customerForm ? (CUSTOMER_ROOTS[command] ?? VENDOR_ROOTS) : VENDOR_ROOTS, {
|
|
@@ -165,7 +178,14 @@ async function run(argv, env) {
|
|
|
165
178
|
assertNoToolingOverride(parseOverrides(options['--overrides']));
|
|
166
179
|
return;
|
|
167
180
|
}
|
|
181
|
+
case 'choose-kernel': {
|
|
182
|
+
const { choice, notice } = chooseKernelCommit({ repoDir: kernelRepoDir(), pin: options['--kernel-pin'] });
|
|
183
|
+
writeOnce(options['--out'], choice);
|
|
184
|
+
if (notice !== undefined) {console.log(notice);}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
168
187
|
case 'freeze-graph': {
|
|
188
|
+
assertKernelCommitSupported({ repoDir: kernelRepoDir(), commit: options['--kernel-sha'], label: `--kernel-sha ${options['--kernel-sha']}` });
|
|
169
189
|
const pins = freezeGraph({
|
|
170
190
|
runtimeSourceIndex: readStrictJsonFile(runtimeImageSourceIndexPath()),
|
|
171
191
|
dataSha: options['--data-sha'],
|
|
@@ -22,9 +22,10 @@ export class UndeclaredSupplementalRuntimeComponentError extends Error {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// The `supplementalRuntimeComponentId` field is read as plain data
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// The `supplementalRuntimeComponentId` field is read as plain data. Since edition
|
|
26
|
+
// protocol v10 the kernel types it (`DispatchedRuntimeComponentSchema`, a union of
|
|
27
|
+
// two strict objects); this projection keeps reading it as data and refuses a target
|
|
28
|
+
// that is neither form, or both.
|
|
28
29
|
const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
|
|
29
30
|
|
|
30
31
|
/** True for the supplemental form of a projected edge. */
|
|
@@ -28,12 +28,15 @@ import { realpathSync } from 'node:fs';
|
|
|
28
28
|
|
|
29
29
|
import { readBoundedNoFollowFile } from './no-follow-file.mjs';
|
|
30
30
|
|
|
31
|
-
/** The edition protocols this library interprets: version -> content digest.
|
|
31
|
+
/** The edition protocols this library interprets: version -> content digest. v10..v14 grow the kernel
|
|
32
|
+
* source closure only (entry subpaths and hashing unchanged), so one library speaks every row. */
|
|
32
33
|
export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
|
|
33
|
-
Object.freeze({
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}),
|
|
34
|
+
Object.freeze({ version: 9, contentDigest: '7cd6d760e7e7caaec3634a08c739debbdeeeaac4f1bad6791246a12f4f248e37' }),
|
|
35
|
+
Object.freeze({ version: 10, contentDigest: '660129065650a8300f7c0ad54f957b4d43507e1a44e093c9812d2b1bbf143104' }),
|
|
36
|
+
Object.freeze({ version: 11, contentDigest: '600451d2253c470c7d21bafe87260dfc3bfd43b6ad3eee2ef4d8b1f7ad08eb6e' }),
|
|
37
|
+
Object.freeze({ version: 12, contentDigest: 'cff51ea06b6ba7d95d02dcd6305b6a55061b428da48ae74dcbff8a5c8107ce41' }),
|
|
38
|
+
Object.freeze({ version: 13, contentDigest: '5f69fe73d1dea1650ef1757204a53ccd4030ade78bfa91959c2d01c937fc0e16' }),
|
|
39
|
+
Object.freeze({ version: 14, contentDigest: '403b6a491b5922df1a423ba8a2d6ae3d3900bb1caff4e8c0caa48de90a47eb9b' }),
|
|
37
40
|
]);
|
|
38
41
|
|
|
39
42
|
/** Where a kernel package publishes its identity and its contract, package-relative. */
|
|
@@ -64,6 +67,12 @@ export const EditionProtocolMismatchReason = Object.freeze({
|
|
|
64
67
|
ContractMismatch: 'contract-mismatch',
|
|
65
68
|
/** The kernel's protocol is not the one compiled into the running verifier. */
|
|
66
69
|
BundleMismatch: 'bundle-mismatch',
|
|
70
|
+
/** No commit on the kernel checkout's first-parent history declares a protocol this library supports. */
|
|
71
|
+
NoSupportedCommit: 'no-supported-commit',
|
|
72
|
+
/** A kernel commit's protocol declaration source exists but does not parse; refused, never skipped. */
|
|
73
|
+
Unparseable: 'unparseable',
|
|
74
|
+
/** An explicit kernel commit is not on the first-parent history of the checkout's develop head. */
|
|
75
|
+
NotOnDevelop: 'not-on-develop',
|
|
67
76
|
});
|
|
68
77
|
|
|
69
78
|
export class EditionProtocolMismatchError extends Error {
|
|
@@ -76,7 +85,8 @@ export class EditionProtocolMismatchError extends Error {
|
|
|
76
85
|
|
|
77
86
|
const describe = ({ version, contentDigest }) => `edition protocol v${version} (${contentDigest})`;
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
/** `v<N>, v<M>`: the versions this library supports, as every refusal names them. */
|
|
89
|
+
export const supportedList = () =>
|
|
80
90
|
SUPPORTED_EDITION_PROTOCOLS.map((protocol) => `v${protocol.version}`).join(', ');
|
|
81
91
|
|
|
82
92
|
/** Refuse an identity that states `provisional: true`; nothing provisional is ever read as a protocol. */
|
|
@@ -147,10 +157,11 @@ function contractIdentity(contractBytes) {
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
/**
|
|
150
|
-
* Refuse an identity this library does not support
|
|
151
|
-
*
|
|
160
|
+
* Refuse an identity this library does not support: a provisional one, an
|
|
161
|
+
* unsupported version, or a supported version at another digest. Returns it.
|
|
152
162
|
*/
|
|
153
|
-
export function
|
|
163
|
+
export function assertSupportedEditionIdentity(identity, source) {
|
|
164
|
+
assertFinalIdentity(identity, source);
|
|
154
165
|
const supported = SUPPORTED_EDITION_PROTOCOLS.find(
|
|
155
166
|
(protocol) => protocol.version === identity.version,
|
|
156
167
|
);
|
|
@@ -169,6 +180,22 @@ export function assertSupportedEditionProtocol({ identity, contractBytes, source
|
|
|
169
180
|
'contract without a new protocol version.',
|
|
170
181
|
);
|
|
171
182
|
}
|
|
183
|
+
return identity;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** True when `identity` is final and is a row of the supported table (version and digest). */
|
|
187
|
+
export const isSupportedEditionIdentity = (identity) =>
|
|
188
|
+
identity?.provisional === false &&
|
|
189
|
+
SUPPORTED_EDITION_PROTOCOLS.some(
|
|
190
|
+
(protocol) => protocol.version === identity.version && protocol.contentDigest === identity.contentDigest,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Refuse an identity this library does not support, and a contract file that
|
|
195
|
+
* does not state that identity. Returns the identity.
|
|
196
|
+
*/
|
|
197
|
+
export function assertSupportedEditionProtocol({ identity, contractBytes, source }) {
|
|
198
|
+
assertSupportedEditionIdentity(identity, source);
|
|
172
199
|
const stated = contractIdentity(contractBytes);
|
|
173
200
|
if (stated?.version !== identity.version || stated?.contentDigest !== identity.contentDigest) {
|
|
174
201
|
throw new EditionProtocolMismatchError(
|
|
@@ -38,6 +38,8 @@ const ROOT_MEANING = Object.freeze({
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
const KERNEL_PACKAGE_SUBPATH = ['packages', 'kernel', 'kernel-contracts'];
|
|
41
|
+
/** The kernel package's path inside the kernel-contracts repository, `/`-separated (a git tree path). */
|
|
42
|
+
export const KERNEL_PACKAGE_REPO_PATH = KERNEL_PACKAGE_SUBPATH.join('/');
|
|
41
43
|
|
|
42
44
|
let configured = Object.freeze({});
|
|
43
45
|
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// kernel-protocol-choice — which kernel commit a develop edition runs.
|
|
2
|
+
//
|
|
3
|
+
// The CLI runs the kernel's code and refuses a protocol it does not support
|
|
4
|
+
// (edition-protocol.mjs), so a develop edition that always took the kernel's
|
|
5
|
+
// develop HEAD would break on every kernel protocol bump until a CLI release
|
|
6
|
+
// caught up. Commits that declare the same protocol identity (version AND
|
|
7
|
+
// digest) are interchangeable by the digest contract, so the develop kernel is
|
|
8
|
+
// the NEWEST commit on the checkout's first-parent history whose declared
|
|
9
|
+
// identity is in this CLI's supported set: a deterministic choice, never a
|
|
10
|
+
// fallback.
|
|
11
|
+
//
|
|
12
|
+
// - head supported -> the head (reason head-supported)
|
|
13
|
+
// - head ahead of the CLI -> the newest supported (reason head-ahead, held)
|
|
14
|
+
// - an explicit 40-hex pin -> the pin, still checked (reason explicit-pin); it must be on the first-parent
|
|
15
|
+
// history of the develop head (refs/remotes/origin/develop), else not-on-develop
|
|
16
|
+
// - nothing supported -> refused (EditionProtocolMismatchError, no-supported-commit); never the head.
|
|
17
|
+
// The walk ends at the first commit with no declaration file (it predates the protocol).
|
|
18
|
+
// - a declaration that exists but does not parse, at the head or any walked commit
|
|
19
|
+
// -> refused (EditionProtocolMismatchError, unparseable) naming the commit; never skipped
|
|
20
|
+
//
|
|
21
|
+
// The declared identity is read from each commit's SOURCE
|
|
22
|
+
// (`<kernel package>/src/edition-protocol/lib/edition-protocol.ts`, the literal
|
|
23
|
+
// `EDITION_PROTOCOL = Object.freeze({ version, provisional, contentDigest })`),
|
|
24
|
+
// because the history is not built; the built kernel is checked again from its
|
|
25
|
+
// dist files by every loader. Reads git objects only; writes nothing.
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
EditionProtocolMismatchError,
|
|
31
|
+
EditionProtocolMismatchReason,
|
|
32
|
+
assertSupportedEditionIdentity,
|
|
33
|
+
isSupportedEditionIdentity,
|
|
34
|
+
supportedList,
|
|
35
|
+
} from './edition-protocol.mjs';
|
|
36
|
+
import { KERNEL_PACKAGE_REPO_PATH } from './edition-workspace.mjs';
|
|
37
|
+
|
|
38
|
+
/** The kernel source file that declares the edition protocol identity, repository-relative. */
|
|
39
|
+
export const KERNEL_EDITION_PROTOCOL_SOURCE = `${KERNEL_PACKAGE_REPO_PATH}/src/edition-protocol/lib/edition-protocol.ts`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The kernel checkout's develop head: the remote-tracking ref a full-history checkout (`fetch-depth: 0`)
|
|
43
|
+
* always has. Every choice and every explicit commit is judged against its first-parent history.
|
|
44
|
+
*/
|
|
45
|
+
export const KERNEL_DEVELOP_REF = 'refs/remotes/origin/develop';
|
|
46
|
+
|
|
47
|
+
/** Why a kernel commit was chosen. A closed set; `held` is exactly `head-ahead`. */
|
|
48
|
+
export const KernelChoiceReason = Object.freeze({
|
|
49
|
+
HeadSupported: 'head-supported',
|
|
50
|
+
HeadAhead: 'head-ahead',
|
|
51
|
+
ExplicitPin: 'explicit-pin',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/;
|
|
55
|
+
const MAX_GIT_OUTPUT = 256 * 1024 * 1024;
|
|
56
|
+
const DECLARATION = /export const EDITION_PROTOCOL\b[^=]*=\s*Object\.freeze\(\{([^}]*)\}\)/g;
|
|
57
|
+
const FIELDS = Object.freeze([
|
|
58
|
+
['version', /\bversion:\s*([1-9][0-9]*)\s*(?:,|$)/],
|
|
59
|
+
['provisional', /\bprovisional:\s*(true|false)\s*(?:,|$)/],
|
|
60
|
+
['contentDigest', /\bcontentDigest:\s*'([a-f0-9]{64})'\s*(?:,|$)/],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
/** A declaration source that exists but whose `EDITION_PROTOCOL` literal is absent or not exactly the three fields. */
|
|
64
|
+
const malformed = (label) =>
|
|
65
|
+
new EditionProtocolMismatchError(
|
|
66
|
+
EditionProtocolMismatchReason.Unparseable,
|
|
67
|
+
`${label} does not parse: it must declare exactly one EDITION_PROTOCOL = Object.freeze({ version: <positive integer>, ` +
|
|
68
|
+
"provisional: <boolean>, contentDigest: '<64 lowercase hex>' })",
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The identity `{contentDigest, provisional, version}` the source text declares. Strict: exactly one
|
|
73
|
+
* declaration, each field exactly once, nothing else in the literal. Throws Unparseable otherwise.
|
|
74
|
+
*/
|
|
75
|
+
export function parseDeclaredEditionProtocol(text, label) {
|
|
76
|
+
const declarations = [...text.matchAll(DECLARATION)];
|
|
77
|
+
if (declarations.length !== 1) {throw malformed(label);}
|
|
78
|
+
let rest = declarations[0][1];
|
|
79
|
+
const fields = {};
|
|
80
|
+
for (const [name, pattern] of FIELDS) {
|
|
81
|
+
const all = rest.match(new RegExp(pattern.source, 'g')) ?? [];
|
|
82
|
+
if (all.length !== 1) {throw malformed(label);}
|
|
83
|
+
fields[name] = all[0].match(pattern)[1];
|
|
84
|
+
rest = rest.replace(all[0], '');
|
|
85
|
+
}
|
|
86
|
+
if (rest.replace(/\/\/[^\n]*/g, '').trim() !== '') {throw malformed(label);}
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
contentDigest: fields.contentDigest,
|
|
89
|
+
provisional: fields.provisional === 'true',
|
|
90
|
+
version: Number(fields.version),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const describeShort = (identity) =>
|
|
95
|
+
identity === undefined ? 'no declared protocol' : `v${identity.version}${identity.provisional ? '-provisional' : ''}`;
|
|
96
|
+
|
|
97
|
+
/** Git against one checkout; `runGit(args, input?)` returns stdout as a string. */
|
|
98
|
+
function gitIn(repoDir, runGit) {
|
|
99
|
+
return (args, input) =>
|
|
100
|
+
runGit('git', ['-C', repoDir, ...args], {
|
|
101
|
+
encoding: 'utf8',
|
|
102
|
+
maxBuffer: MAX_GIT_OUTPUT,
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
...(input === undefined ? {} : { input }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assertCommit(git, sha, label) {
|
|
109
|
+
if (typeof sha !== 'string' || !SHA_PATTERN.test(sha)) {
|
|
110
|
+
throw new Error(`${label} is not a 40-hex commit SHA: ${JSON.stringify(sha)}`);
|
|
111
|
+
}
|
|
112
|
+
let resolved;
|
|
113
|
+
try {
|
|
114
|
+
resolved = git(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`]).trim();
|
|
115
|
+
} catch {
|
|
116
|
+
resolved = '';
|
|
117
|
+
}
|
|
118
|
+
if (resolved !== sha) {throw new Error(`${label} is not a commit in the kernel checkout`);}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The commit {@link KERNEL_DEVELOP_REF} names; refused when the checkout has no such ref. */
|
|
122
|
+
function readDevelopHead(git) {
|
|
123
|
+
let head;
|
|
124
|
+
try {
|
|
125
|
+
head = git(['rev-parse', '--verify', '--quiet', `${KERNEL_DEVELOP_REF}^{commit}`]).trim();
|
|
126
|
+
} catch {
|
|
127
|
+
head = '';
|
|
128
|
+
}
|
|
129
|
+
if (!SHA_PATTERN.test(head)) {
|
|
130
|
+
throw new Error(`the kernel checkout has no ${KERNEL_DEVELOP_REF}; check it out with full history (fetch-depth: 0)`);
|
|
131
|
+
}
|
|
132
|
+
return head;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The first-parent history of `head`, newest first. */
|
|
136
|
+
const firstParentHistory = (git, head) =>
|
|
137
|
+
git(['rev-list', '--first-parent', head]).split('\n').filter((line) => line.length > 0);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The declaring blob of every commit, by one `git cat-file --batch-check`: a Map commit -> blob sha, or
|
|
141
|
+
* undefined when the commit has no such file.
|
|
142
|
+
*/
|
|
143
|
+
function declaringBlobs(git, commits) {
|
|
144
|
+
const input = commits.map((commit) => `${commit}:${KERNEL_EDITION_PROTOCOL_SOURCE}\n`).join('');
|
|
145
|
+
const lines = git(['cat-file', '--batch-check'], input).split('\n').filter((line) => line.length > 0);
|
|
146
|
+
if (lines.length !== commits.length) {
|
|
147
|
+
throw new Error(`git cat-file answered ${lines.length} lines for ${commits.length} kernel commits`);
|
|
148
|
+
}
|
|
149
|
+
return new Map(
|
|
150
|
+
commits.map((commit, index) => {
|
|
151
|
+
const [sha, type] = lines[index].split(' ');
|
|
152
|
+
return [commit, type === 'blob' && SHA_PATTERN.test(sha) ? sha : undefined];
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** A reader of the declared identity per blob (memoized); `undefined` for an absent file. Throws Unparseable. */
|
|
158
|
+
function identityReader(git) {
|
|
159
|
+
const byBlob = new Map();
|
|
160
|
+
return (commit, blob) => {
|
|
161
|
+
if (blob === undefined) {return undefined;}
|
|
162
|
+
if (!byBlob.has(blob)) {
|
|
163
|
+
const label = `the kernel commit ${commit}: ${KERNEL_EDITION_PROTOCOL_SOURCE}`;
|
|
164
|
+
byBlob.set(blob, parseDeclaredEditionProtocol(git(['cat-file', 'blob', blob]), label));
|
|
165
|
+
}
|
|
166
|
+
return byBlob.get(blob);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Refuse a kernel commit whose declared identity this CLI does not support (absent, provisional, another
|
|
172
|
+
* version, or a supported version at another digest), naming `label`. The one check behind an explicit pin
|
|
173
|
+
* and `freeze-graph --kernel-sha`. Returns the identity.
|
|
174
|
+
*/
|
|
175
|
+
export function assertKernelCommitSupported({ repoDir, commit, label = `the kernel commit ${commit}`, runGit = execFileSync }) {
|
|
176
|
+
const git = gitIn(repoDir, runGit);
|
|
177
|
+
assertCommit(git, commit, label);
|
|
178
|
+
const developHead = readDevelopHead(git);
|
|
179
|
+
if (!firstParentHistory(git, developHead).includes(commit)) {
|
|
180
|
+
throw new EditionProtocolMismatchError(
|
|
181
|
+
EditionProtocolMismatchReason.NotOnDevelop,
|
|
182
|
+
`${label} is not on the first-parent history of the kernel develop head ${developHead} (${KERNEL_DEVELOP_REF}); ` +
|
|
183
|
+
'an explicit kernel commit must be one that kernel develop actually passed through.',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const identity = identityReader(git)(commit, declaringBlobs(git, [commit]).get(commit));
|
|
187
|
+
if (identity === undefined) {
|
|
188
|
+
throw new EditionProtocolMismatchError(
|
|
189
|
+
EditionProtocolMismatchReason.Undeclared,
|
|
190
|
+
`${label} declares no edition protocol (${KERNEL_EDITION_PROTOCOL_SOURCE} is absent); this CLI supports ${supportedList()}.`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return assertSupportedEditionIdentity(identity, `${label} (${KERNEL_EDITION_PROTOCOL_SOURCE})`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Choose the develop kernel commit in the checkout at `repoDir` (fetched with full history).
|
|
198
|
+
*
|
|
199
|
+
* @param {object} request
|
|
200
|
+
* @param {string} request.repoDir the kernel-contracts checkout
|
|
201
|
+
* @param {string} [request.pin] an explicit 40-hex commit; it wins, and is still checked
|
|
202
|
+
* @returns {{ choice: {chosenSha: string, headSha: string, held: boolean, reason: string}, notice?: string }}
|
|
203
|
+
* `choice` is what is recorded; `notice` (only when held) is the run-summary line, never persisted.
|
|
204
|
+
*/
|
|
205
|
+
export function chooseKernelCommit({ repoDir, pin, runGit = execFileSync }) {
|
|
206
|
+
const git = gitIn(repoDir, runGit);
|
|
207
|
+
const headSha = readDevelopHead(git);
|
|
208
|
+
if (pin !== undefined) {
|
|
209
|
+
assertKernelCommitSupported({ repoDir, commit: pin, label: `the kernel pin ${pin}`, runGit });
|
|
210
|
+
return { choice: { chosenSha: pin, headSha, held: false, reason: KernelChoiceReason.ExplicitPin } };
|
|
211
|
+
}
|
|
212
|
+
const commits = firstParentHistory(git, headSha);
|
|
213
|
+
const blobs = declaringBlobs(git, commits);
|
|
214
|
+
const identityAt = identityReader(git);
|
|
215
|
+
// Fail closed: an unparseable declaration (head or walked) is refused by name, never skipped; a commit
|
|
216
|
+
// with no declaration file predates the protocol, so the walk ends there.
|
|
217
|
+
const headIdentity = identityAt(headSha, blobs.get(headSha));
|
|
218
|
+
if (isSupportedEditionIdentity(headIdentity)) {
|
|
219
|
+
return { choice: { chosenSha: headSha, headSha, held: false, reason: KernelChoiceReason.HeadSupported } };
|
|
220
|
+
}
|
|
221
|
+
for (const commit of headIdentity === undefined ? [] : commits.slice(1)) {
|
|
222
|
+
const identity = identityAt(commit, blobs.get(commit));
|
|
223
|
+
if (identity === undefined) {break;}
|
|
224
|
+
if (isSupportedEditionIdentity(identity)) {
|
|
225
|
+
return {
|
|
226
|
+
choice: { chosenSha: commit, headSha, held: true, reason: KernelChoiceReason.HeadAhead },
|
|
227
|
+
notice:
|
|
228
|
+
`kernel held at ${describeShort(identity)} (${commit.slice(0, 12)}), ` +
|
|
229
|
+
`head at ${describeShort(headIdentity)} (${headSha.slice(0, 12)}): raise the CLI`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw new EditionProtocolMismatchError(
|
|
234
|
+
EditionProtocolMismatchReason.NoSupportedCommit,
|
|
235
|
+
`no commit on the first-parent history of the kernel head ${headSha} declares an edition protocol this CLI ` +
|
|
236
|
+
`supports (this CLI supports ${supportedList()}; the head declares ${describeShort(headIdentity)}` +
|
|
237
|
+
`${headIdentity === undefined ? '' : ` at ${headIdentity.contentDigest}`}). Raise the CLI; the head is never taken instead.`,
|
|
238
|
+
);
|
|
239
|
+
}
|
package/edition/lib.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import { isKernelCapabilityDomain } from './kernel-capability-domain.mjs';
|
|
|
40
40
|
import {
|
|
41
41
|
PRINCIPAL_PRIVILEGE_FIELDS,
|
|
42
42
|
assertPrivilegesHavePrincipals,
|
|
43
|
+
componentInstalledOperationRequests,
|
|
43
44
|
componentOperationRequirements,
|
|
44
45
|
componentOptionalOperationRequirements,
|
|
45
46
|
componentOrganizationSweepRequirements,
|
|
@@ -686,6 +687,64 @@ const kernelDistributionDist = () => kernelDistPath('distribution', 'index.js');
|
|
|
686
687
|
// through. A sibling of the distribution dist, loaded beside it.
|
|
687
688
|
const kernelProcessTopologyDist = () => kernelDistPath('process-topology', 'index.js');
|
|
688
689
|
|
|
690
|
+
/** A composed-process subject, on a kernel that cannot write image-lock schemaVersion 4. */
|
|
691
|
+
export class ComposedProcessNeedsLockV4Error extends Error {
|
|
692
|
+
constructor(message) {
|
|
693
|
+
super(message);
|
|
694
|
+
this.name = 'ComposedProcessNeedsLockV4Error';
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** The image-lock revision of a kernel that predates `IMAGE_LOCK_WRITE_SCHEMA_VERSION` (edition protocol < v14). */
|
|
699
|
+
const PRE_V14_IMAGE_LOCK_SCHEMA_VERSION = 3;
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Refuse a composed-process subject when the loaded kernel predates image-lock
|
|
703
|
+
* schemaVersion 4 (it exports no `IMAGE_LOCK_WRITE_SCHEMA_VERSION`): such a
|
|
704
|
+
* kernel has no `members[]`, so it cannot say which services the composed
|
|
705
|
+
* image carries. There is no fallback.
|
|
706
|
+
*/
|
|
707
|
+
export function assertKernelSealsComposedMembership(schemas, subjects, label) {
|
|
708
|
+
if (Number.isInteger(schemas.IMAGE_LOCK_WRITE_SCHEMA_VERSION)) {return;}
|
|
709
|
+
const composed = (subjects ?? []).find((subject) => subject.kind === 'composed-process');
|
|
710
|
+
if (composed === undefined) {return;}
|
|
711
|
+
throw new ComposedProcessNeedsLockV4Error(
|
|
712
|
+
`${label}: composed-process subject "${composed.id}" — a composed-process subject needs a kernel that ` +
|
|
713
|
+
'writes image-lock schemaVersion 4 (edition protocol >= v14); the loaded kernel ' +
|
|
714
|
+
`(${kernelRepoDir()}) exports no IMAGE_LOCK_WRITE_SCHEMA_VERSION`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* The READ side of the same rule, for a kernel before image-lock
|
|
720
|
+
* schemaVersion 4 (no `ImageLockSchemaVersion`): it reads only schemaVersion
|
|
721
|
+
* 3, which names no composed members, so a v3 lock carrying a composed-process
|
|
722
|
+
* subject cannot say which services run from it and is refused (worded as the
|
|
723
|
+
* kernel's own `LegacyComposedImageLockError`).
|
|
724
|
+
*/
|
|
725
|
+
export function assertLegacyLockHasNoComposedSubject(lock) {
|
|
726
|
+
const composed = (lock.subjects ?? []).find((subject) => subject.kind === 'composed-process');
|
|
727
|
+
if (composed === undefined) {return;}
|
|
728
|
+
throw new ComposedProcessNeedsLockV4Error(
|
|
729
|
+
`distribution "${lock.distributionId}": subjects "${composed.id}" is a schemaVersion ` +
|
|
730
|
+
`${String(lock.schemaVersion)} composed-process subject, which names no members: regenerate with a CLI ` +
|
|
731
|
+
'that writes image-lock schemaVersion 4, against a kernel at edition protocol >= v14',
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* The image-lock schemaVersion a writer emits for `subjects`: the loaded
|
|
737
|
+
* kernel's `IMAGE_LOCK_WRITE_SCHEMA_VERSION`. A kernel before edition protocol
|
|
738
|
+
* v14 exports none and writes only schemaVersion 3, which is allowed only when
|
|
739
|
+
* there is NO composed-process subject
|
|
740
|
+
* ({@link ComposedProcessNeedsLockV4Error} otherwise).
|
|
741
|
+
*/
|
|
742
|
+
export function imageLockWriteSchemaVersion(schemas, subjects, label = 'images.lock.json') {
|
|
743
|
+
assertKernelSealsComposedMembership(schemas, subjects, label);
|
|
744
|
+
const version = schemas.IMAGE_LOCK_WRITE_SCHEMA_VERSION;
|
|
745
|
+
return Number.isInteger(version) ? version : PRE_V14_IMAGE_LOCK_SCHEMA_VERSION;
|
|
746
|
+
}
|
|
747
|
+
|
|
689
748
|
/**
|
|
690
749
|
* Dynamically import the BUILT kernel distribution schemas. The kernel package
|
|
691
750
|
* is NOT linked into the repo-root node_modules (it lives in its own sub-
|
|
@@ -737,6 +796,8 @@ export async function loadKernelDistributionSchemas() {
|
|
|
737
796
|
parseOciDigest,
|
|
738
797
|
parseGitTreeId,
|
|
739
798
|
parseImageLock,
|
|
799
|
+
IMAGE_LOCK_WRITE_SCHEMA_VERSION,
|
|
800
|
+
ImageLockSchemaVersion,
|
|
740
801
|
parseImageProducerStatement,
|
|
741
802
|
parseDistributionImagesStatement,
|
|
742
803
|
parseImageDeliveryEntitlementStatement,
|
|
@@ -789,6 +850,8 @@ export async function loadKernelDistributionSchemas() {
|
|
|
789
850
|
admitOperationGrants,
|
|
790
851
|
admitOrganizationSweeps,
|
|
791
852
|
contractRequiredServiceTargets,
|
|
853
|
+
admitInstalledOperationCeiling,
|
|
854
|
+
admitStoreWorkloadCeiling,
|
|
792
855
|
} = mod;
|
|
793
856
|
let topology;
|
|
794
857
|
try {
|
|
@@ -886,6 +949,11 @@ export async function loadKernelDistributionSchemas() {
|
|
|
886
949
|
typeof admitOperationGrants !== 'function' ||
|
|
887
950
|
typeof admitOrganizationSweeps !== 'function' ||
|
|
888
951
|
typeof contractRequiredServiceTargets !== 'function' ||
|
|
952
|
+
// The installed-operation ceiling and the edition's store-workload ceiling,
|
|
953
|
+
// both admitted at the lock (`assertInstalledOperationCeilingAdmitted`,
|
|
954
|
+
// `assertStoreWorkloadCeilingAdmitted`).
|
|
955
|
+
typeof admitInstalledOperationCeiling !== 'function' ||
|
|
956
|
+
typeof admitStoreWorkloadCeiling !== 'function' ||
|
|
889
957
|
typeof distributionImagesPredicateLockRefusal !== 'function' ||
|
|
890
958
|
typeof distributionLockDigest !== 'function' ||
|
|
891
959
|
typeof imageDeliveryEntitlementRefusal !== 'function' ||
|
|
@@ -907,7 +975,7 @@ export async function loadKernelDistributionSchemas() {
|
|
|
907
975
|
`resolveRuntimeImagePlatforms/RuntimeImagePlatformKey/` +
|
|
908
976
|
`kernel contract-authority digest SSOT/compareLockedServiceResources/resolveNexusBindings/describeNexusBindingRefusal/canonicalNexusFacts/` +
|
|
909
977
|
`positive closure (resolveBiomeClosure/platformInvariantRoots/DistributionResolutionMode/provenance)/` +
|
|
910
|
-
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/` +
|
|
978
|
+
`deriveWorkflowSubjects/deriveTrustCeiling (>= 0.71.0)/admitOperationGrants/admitOrganizationSweeps/admitInstalledOperationCeiling/admitStoreWorkloadCeiling/` +
|
|
911
979
|
`defaultTrustDomain/FIRST_PARTY_TRUST_DOMAIN/ProcessTrustPolicySchema/DistributionTrustPolicySchema/LockedStorePublisherSchema/` +
|
|
912
980
|
`resolveFirstPartyCapabilityDomains/DistributionVisibility/` +
|
|
913
981
|
`BiomeInstallPolicy/BiomeLaunchMode/ImageSourceHashAlgorithm/runtime-image enums — ` +
|
|
@@ -933,6 +1001,8 @@ export async function loadKernelDistributionSchemas() {
|
|
|
933
1001
|
parseOciDigest,
|
|
934
1002
|
parseGitTreeId,
|
|
935
1003
|
parseImageLock,
|
|
1004
|
+
IMAGE_LOCK_WRITE_SCHEMA_VERSION,
|
|
1005
|
+
ImageLockSchemaVersion,
|
|
936
1006
|
parseImageProducerStatement,
|
|
937
1007
|
parseDistributionImagesStatement,
|
|
938
1008
|
parseImageDeliveryEntitlementStatement,
|
|
@@ -992,6 +1062,8 @@ export async function loadKernelDistributionSchemas() {
|
|
|
992
1062
|
admitOperationGrants,
|
|
993
1063
|
admitOrganizationSweeps,
|
|
994
1064
|
contractRequiredServiceTargets,
|
|
1065
|
+
admitInstalledOperationCeiling,
|
|
1066
|
+
admitStoreWorkloadCeiling,
|
|
995
1067
|
distributionImagesPredicateLockRefusal,
|
|
996
1068
|
imageDeliveryEntitlementRefusal,
|
|
997
1069
|
producerWorkflowPinRefusal,
|
|
@@ -1623,7 +1695,7 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1623
1695
|
'resolve no worker image: a worker declares artifact.kind "oci-image" or "shared-oci-image"',
|
|
1624
1696
|
);
|
|
1625
1697
|
}
|
|
1626
|
-
// A privilege (`requiresOperations`, `optionalOperations`, `requiresOrganizationSweeps`) is admitted for a lock workload
|
|
1698
|
+
// A privilege (`requiresOperations`, `optionalOperations`, `requestsInstalledOperations`, `requiresOrganizationSweeps`) is admitted for a lock workload
|
|
1627
1699
|
// principal only; on any other component it is refused, never dropped.
|
|
1628
1700
|
assertPrivilegesHavePrincipals(components, imageComponents, rel);
|
|
1629
1701
|
// A biome must declare its images in EXACTLY ONE shape. Declaring both a
|
|
@@ -1674,6 +1746,10 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1674
1746
|
// (`optionalOperations`), keyed like `operationRequirements`. Declared
|
|
1675
1747
|
// intent only: the seal grants, leaves ungranted or refuses each one.
|
|
1676
1748
|
const optionalOperationRequirements = {};
|
|
1749
|
+
// `component → the operations it asks to exercise for the installing
|
|
1750
|
+
// organization` (`requestsInstalledOperations`), keyed like
|
|
1751
|
+
// `operationRequirements`. Declared intent only: the lock admits the ceiling.
|
|
1752
|
+
const installedOperationRequests = {};
|
|
1677
1753
|
// `component → the organization sweeps it may hold`
|
|
1678
1754
|
// (`requiresOrganizationSweeps`), keyed like `operationRequirements`.
|
|
1679
1755
|
// Declared intent only: the seal admits or refuses each one.
|
|
@@ -1867,6 +1943,8 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
1867
1943
|
if (operations !== null) {operationRequirements[name] = operations;}
|
|
1868
1944
|
const optionalOperations = componentOptionalOperationRequirements(component, where);
|
|
1869
1945
|
if (optionalOperations !== null) {optionalOperationRequirements[name] = optionalOperations;}
|
|
1946
|
+
const installed = componentInstalledOperationRequests(component, where);
|
|
1947
|
+
if (installed !== null) {installedOperationRequests[name] = installed;}
|
|
1870
1948
|
const sweeps = componentOrganizationSweepRequirements(component, where);
|
|
1871
1949
|
if (sweeps !== null) {organizationSweepRequirements[name] = sweeps;}
|
|
1872
1950
|
const queues = componentWorkerTaskQueues(component, protocol, where);
|
|
@@ -2311,6 +2389,17 @@ export async function manifestToIndexEntry({ raw, generatedIndex, rel, repo, pat
|
|
|
2311
2389
|
),
|
|
2312
2390
|
}
|
|
2313
2391
|
: {}),
|
|
2392
|
+
// The operations each workload component asks to exercise for the
|
|
2393
|
+
// installing organization; omitted when none declares any.
|
|
2394
|
+
...(Object.keys(installedOperationRequests).length > 0
|
|
2395
|
+
? {
|
|
2396
|
+
installedOperationRequests: Object.fromEntries(
|
|
2397
|
+
Object.entries(installedOperationRequests).sort(([a], [b]) =>
|
|
2398
|
+
compareStrings(a, b),
|
|
2399
|
+
),
|
|
2400
|
+
),
|
|
2401
|
+
}
|
|
2402
|
+
: {}),
|
|
2314
2403
|
// The organization sweeps each workload component may hold; omitted when
|
|
2315
2404
|
// none declares any.
|
|
2316
2405
|
...(Object.keys(organizationSweepRequirements).length > 0
|
|
@@ -2920,8 +3009,16 @@ export function validateImmutableImageLock(lock, schemas) {
|
|
|
2920
3009
|
if (!schemas) {
|
|
2921
3010
|
throw new Error('trusted kernel distribution schemas are required');
|
|
2922
3011
|
}
|
|
3012
|
+
assertKernelSealsComposedMembership(schemas, lock.subjects, `distribution "${lock.distributionId}"`);
|
|
3013
|
+
// The revisions the loaded kernel reads (`ImageLockSchemaVersion`: 3 without
|
|
3014
|
+
// composed membership, 4 with it); a kernel before image-lock v4 reads only 3.
|
|
3015
|
+
if (schemas.ImageLockSchemaVersion === undefined) {assertLegacyLockHasNoComposedSubject(lock);}
|
|
3016
|
+
const readableVersions =
|
|
3017
|
+
schemas.ImageLockSchemaVersion === undefined
|
|
3018
|
+
? [PRE_V14_IMAGE_LOCK_SCHEMA_VERSION]
|
|
3019
|
+
: Object.values(schemas.ImageLockSchemaVersion).filter((version) => Number.isInteger(version));
|
|
2923
3020
|
if (
|
|
2924
|
-
lock.schemaVersion
|
|
3021
|
+
!readableVersions.includes(lock.schemaVersion) ||
|
|
2925
3022
|
lock.sourceHashAlgorithm !== schemas.ImageSourceHashAlgorithm.ImageInputV2
|
|
2926
3023
|
) {
|
|
2927
3024
|
throw new Error(
|