@xemahq/xema 0.44.1 → 0.45.1

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.
@@ -189,7 +189,7 @@ exports.INFRA_CATALOG = Object.freeze({
189
189
  exports.INFRA_COMPONENTS = Object.freeze(Object.values(schema_1.InfraComponent));
190
190
  exports.OBJECT_STORE_BUCKETS = Object.freeze([
191
191
  'session-session-attachments',
192
- 'session-opencode-sessions',
192
+ 'session-session-snapshots',
193
193
  'kb-knowledge-base-pages',
194
194
  'plugin-bundles',
195
195
  'artifact-store',
package/edition/README.md CHANGED
@@ -142,7 +142,7 @@ The edition library builds, validates and verifies evidence statements; it never
142
142
 
143
143
  ## Kernel protocol and pins
144
144
 
145
- 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.
145
+ 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 v16 and v19 to v20 (the versions the kernel declared and contracts published), each pinned to its digest and vendored (v10 to v20 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.
146
146
 
147
147
  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`.
148
148
 
@@ -12,7 +12,7 @@ import { promisify } from 'node:util';
12
12
 
13
13
  import { AttestationStatus, DEVELOP_EVIDENCE_CHANNEL } from './develop-coverage-verdict.mjs';
14
14
  import { COSIGN_ENV, policyCandidates, sha256Digest, validatePinnedTrustedRoot, verifySigstoreEvidenceDescriptor } from './image-evidence.mjs';
15
- import { resolveImageSourceEvidenceReferrer } from './oci-image-source-evidence.mjs';
15
+ import { resolveImageSourceEvidenceReferrer, writeForeignAuthorityReferrerReport } from './oci-image-source-evidence.mjs';
16
16
  import { BUNDLE_MEDIA_TYPE } from './produce-image-source-evidence.mjs';
17
17
  import { loadRegistryCredentialDeclarations, resolveRegistryCredential, splitImageRef } from './registry-credentials.mjs';
18
18
  import { isNotPublishedYet } from './seal-image-digests.mjs';
@@ -81,13 +81,14 @@ function basicAuthParts(credential) {
81
81
  * @param {string} request.policyRoot the directory the policy's trusted root is read from (the data root)
82
82
  * @param {object} request.trustedCosign the digest-pinned cosign handle (prepareTrustedCosignBinary)
83
83
  * @param {object} request.schemas kernel distribution schemas
84
+ * @param {object} request.expectedContractAuthority the kernel contract authority the data root pins
84
85
  * @param {string} request.dataRoot declares how each registry is read
85
86
  * @param {object} request.env environment the registry credentials are named in
86
87
  * @param {Function} [request.runCosign] (path, args, env) => { code, stdout, stderr }; replaced by tests
87
88
  * @param {Function} [request.fetchImpl] @param {Function} [request.run] oras runner, replaced by tests
88
89
  * @param {Function} [request.clientFor] (authority) => registry client, replaced by tests
89
90
  */
90
- export function createCoverageReaders({ policy, policyRoot, trustedCosign, schemas, dataRoot, env, runCosign = defaultRunCosign, fetchImpl, run, clientFor: injectedClientFor }) {
91
+ export function createCoverageReaders({ policy, policyRoot, trustedCosign, schemas, expectedContractAuthority, dataRoot, env, runCosign = defaultRunCosign, fetchImpl, run, clientFor: injectedClientFor }) {
91
92
  const cosignPath = assertPreparedExecutable(trustedCosign, 'cosign');
92
93
  const declarations = injectedClientFor === undefined ? loadRegistryCredentialDeclarations(dataRoot) : null;
93
94
  const scratch = mkdtempSync(join(tmpdir(), 'xema-coverage-'));
@@ -141,6 +142,8 @@ export function createCoverageReaders({ policy, policyRoot, trustedCosign, schem
141
142
  imageRepository: subject.imageRef,
142
143
  subjectDigest: digest,
143
144
  schemas,
145
+ expectedContractAuthority,
146
+ onForeignAuthorityReferrer: writeForeignAuthorityReferrerReport,
144
147
  });
145
148
  if (evidence === null) {return { status: AttestationStatus.None };}
146
149
  if (rows.length === 0) {return { status: AttestationStatus.NoRow };}
@@ -8,6 +8,7 @@ import { collectCoverage, coverageExitCode, renderCoverageTable } from './develo
8
8
  import { DEVELOP_EDITION_STEPS, resolveDevelopSnapshot } from './develop-edition-resolve.mjs';
9
9
  import { loadDeclaredPinnedPolicy } from './evidence-trust-policy.mjs';
10
10
  import { prepareTrustedCosignBinary } from './image-evidence.mjs';
11
+ import { readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
11
12
 
12
13
  const stderrProgress = (line) => process.stderr.write(`${line}\n`);
13
14
 
@@ -34,7 +35,7 @@ export async function runDevelopCoverage({
34
35
  const { schemas, inputs, lock } = await resolveDevelopSnapshot({ id, pins, steps, phase: (name) => progress(`[coverage] ${name}`) });
35
36
  const { imageLock } = await steps.resolveImages(id, { inputs, lock });
36
37
  progress(`[coverage] image content hashes computed (${imageLock.subjects.length} subjects)`);
37
- const readers = createCoverageReaders({ policy, policyRoot: dataRoot, trustedCosign, schemas, dataRoot, env, ...readerOptions });
38
+ const readers = createCoverageReaders({ policy, policyRoot: dataRoot, trustedCosign, schemas, expectedContractAuthority: readPinnedContractAuthority({ dataRoot, schemas }), dataRoot, env, ...readerOptions });
38
39
  try {
39
40
  const report = await collectCoverage({ distributionId: id, subjects: imageLock.subjects, policy, readers, progress });
40
41
  write(json ? `${JSON.stringify(report, null, 2)}\n` : renderCoverageTable(report));
@@ -62,6 +62,7 @@ import { readBoundedNoFollowFile } from './no-follow-file.mjs';
62
62
  import {
63
63
  createRegistryClientResolver,
64
64
  resolveImageSourceEvidenceReferrer,
65
+ writeForeignAuthorityReferrerReport,
65
66
  } from './oci-image-source-evidence.mjs';
66
67
  import { BUNDLE_MEDIA_TYPE, canonicalSignedBundleBytes, cosignVerifyAttestationArgs, validateProducedBundle } from './produce-image-source-evidence.mjs';
67
68
  import { splitImageRef } from './registry-credentials.mjs';
@@ -499,6 +500,8 @@ export async function resolveProducerEvidence({
499
500
  imageRepository: subject.imageRef,
500
501
  subjectDigest: image.digest,
501
502
  schemas,
503
+ expectedContractAuthority,
504
+ onForeignAuthorityReferrer: writeForeignAuthorityReferrerReport,
502
505
  });
503
506
  if (ociEvidence === null) {
504
507
  throw new Error(
@@ -28,7 +28,7 @@ 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. v10..v14 grow the kernel
31
+ /** The edition protocols this library interprets: version -> content digest. v10..v20 grow the kernel
32
32
  * source closure only (entry subpaths and hashing unchanged), so one library speaks every row. */
33
33
  export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
34
34
  Object.freeze({ version: 9, contentDigest: '7cd6d760e7e7caaec3634a08c739debbdeeeaac4f1bad6791246a12f4f248e37' }),
@@ -37,6 +37,10 @@ export const SUPPORTED_EDITION_PROTOCOLS = Object.freeze([
37
37
  Object.freeze({ version: 12, contentDigest: 'cff51ea06b6ba7d95d02dcd6305b6a55061b428da48ae74dcbff8a5c8107ce41' }),
38
38
  Object.freeze({ version: 13, contentDigest: '5f69fe73d1dea1650ef1757204a53ccd4030ade78bfa91959c2d01c937fc0e16' }),
39
39
  Object.freeze({ version: 14, contentDigest: '403b6a491b5922df1a423ba8a2d6ae3d3900bb1caff4e8c0caa48de90a47eb9b' }),
40
+ Object.freeze({ version: 15, contentDigest: 'c66191703abfd9b532d8b6c15476bede2a5752af5bea5032e839fb9d6303a2d1' }),
41
+ Object.freeze({ version: 16, contentDigest: '2e65c280a399304ebb438e4e9829bcb4688f36a3c6a5e8b35b679cf69c689f55' }),
42
+ Object.freeze({ version: 19, contentDigest: '66dab3dcf18c3a587f073808cc91165cc513eabb237b35439ff6cfe2b312b127' }),
43
+ Object.freeze({ version: 20, contentDigest: '22c0e4ba6ee2771e91516ea78806117b5e3e60b76e085b9a9b98e993c1de79f8' }),
40
44
  ]);
41
45
 
42
46
  /** Where a kernel package publishes its identity and its contract, package-relative. */
@@ -39,6 +39,7 @@ import {
39
39
  createRegistryClientResolver,
40
40
  resolveImageSourceEvidenceReferrer,
41
41
  validateMaterializedImageSourceEvidence,
42
+ writeForeignAuthorityReferrerReport,
42
43
  } from './oci-image-source-evidence.mjs';
43
44
  import { REFERRERS_DISTRIBUTION_SPEC } from './oras.mjs';
44
45
  import { assertProducerInput, buildImageSourceStatements, cosignVerifyAttestationArgs, createCosignVerifier, readPinnedContractAuthority } from './produce-image-source-evidence.mjs';
@@ -216,6 +217,7 @@ export async function planOciEvidenceBackfill({
216
217
  editionsRoot: editions,
217
218
  producerRepositoryRoots,
218
219
  schemas,
220
+ expectedContractAuthority,
219
221
  registryClientFactory,
220
222
  }) {
221
223
  const gitIndex = indexCommittedProducerEvidence({
@@ -281,6 +283,8 @@ export async function planOciEvidenceBackfill({
281
283
  imageRepository: item.imageRepository,
282
284
  subjectDigest: item.subjectDigest,
283
285
  schemas,
286
+ expectedContractAuthority,
287
+ onForeignAuthorityReferrer: writeForeignAuthorityReferrerReport,
284
288
  });
285
289
  const reported = {
286
290
  subjectKind: item.subject.kind,
@@ -367,12 +371,14 @@ async function main() {
367
371
  WorkspaceRoot.Kernel,
368
372
  ]);
369
373
  const flags = parseEvidenceCliFlags(rest, { repeated: ['distribution', 'producer-repository-root'] });
374
+ const schemas = await loadKernelDistributionSchemas();
370
375
  const plan = await planOciEvidenceBackfill({
371
376
  distributionIds: flags.requireMany('distribution'),
372
377
  dataRoot: dataRoot(),
373
378
  editionsRoot: editionsRoot(),
374
379
  producerRepositoryRoots: flags.requireMany('producer-repository-root').map((root) => resolve(root)),
375
- schemas: await loadKernelDistributionSchemas(),
380
+ schemas,
381
+ expectedContractAuthority: readPinnedContractAuthority({ dataRoot: dataRoot(), schemas }),
376
382
  });
377
383
  process.stdout.write(serializeJson(plan));
378
384
  // 2: something is still to be produced or attached.
@@ -13,6 +13,7 @@ import {
13
13
  PRODUCER_STATEMENT_MEDIA_TYPE,
14
14
  producerEvidenceLayerFiles,
15
15
  readProducerEvidenceDirectory,
16
+ readRawStatementAuthorityCommits,
16
17
  } from './producer-evidence-bundle.mjs';
17
18
  import {
18
19
  loadRegistryCredentialDeclarations,
@@ -156,9 +157,39 @@ export function validateMaterializedImageSourceEvidence({
156
157
  return { ...evidence, created };
157
158
  }
158
159
 
160
+ function assertExpectedAuthorityCommit(value) {
161
+ if (typeof value !== 'string' || value.length === 0) {
162
+ throw new Error('the expected kernel contract authority commit is required');
163
+ }
164
+ }
165
+
166
+ /** The artifact states a kernel contract authority other than the one evidence is read under. */
167
+ export class ForeignAuthorityReferrerError extends Error {
168
+ constructor({ artifactDigest, sourceCommit }) {
169
+ super(
170
+ `evidence referrer ${artifactDigest} is stated under kernel contract authority ${sourceCommit}`,
171
+ );
172
+ this.name = 'ForeignAuthorityReferrerError';
173
+ this.artifactDigest = artifactDigest;
174
+ this.sourceCommit = sourceCommit;
175
+ }
176
+ }
177
+
178
+ /** The one stderr line that names a skipped foreign-authority referrer. */
179
+ export function writeForeignAuthorityReferrerReport(skipped) {
180
+ process.stderr.write(
181
+ `[evidence] skipped ${skipped.imageRepository}@${skipped.subjectDigest} referrer ${skipped.artifactDigest}: ` +
182
+ `stated under kernel contract authority ${skipped.sourceCommit}, ` +
183
+ `not ${skipped.expectedAuthorityCommit}\n`,
184
+ );
185
+ }
186
+
159
187
  /**
160
188
  * Fetch, content-address validate and decode one OCI evidence artifact.
161
189
  * `getBlob` verifies every digest again, independently of the manifest.
190
+ * An artifact whose RAW statements state another authority commit than
191
+ * `expectedAuthorityCommit` is refused with `ForeignAuthorityReferrerError`
192
+ * before the strict parse, which is bound to the expected authority.
162
193
  */
163
194
  export async function readImageSourceEvidenceArtifact({
164
195
  client,
@@ -166,7 +197,9 @@ export async function readImageSourceEvidenceArtifact({
166
197
  subjectDigest,
167
198
  artifactDigest,
168
199
  schemas,
200
+ expectedAuthorityCommit,
169
201
  }) {
202
+ assertExpectedAuthorityCommit(expectedAuthorityCommit);
170
203
  const fetched = await client.getManifestByDigest(repository, artifactDigest);
171
204
  const { created, layers } = validateImageSourceEvidenceManifest({
172
205
  manifest: fetched.document,
@@ -184,6 +217,11 @@ export async function readImageSourceEvidenceArtifact({
184
217
  mkdirSync(dirname(path), { recursive: true });
185
218
  writeFileSync(path, bytes, { flag: 'wx', mode: 0o600 });
186
219
  }
220
+ for (const sourceCommit of readRawStatementAuthorityCommits({ directory })) {
221
+ if (sourceCommit !== expectedAuthorityCommit) {
222
+ throw new ForeignAuthorityReferrerError({ artifactDigest, sourceCommit });
223
+ }
224
+ }
187
225
  const evidence = validateMaterializedImageSourceEvidence({
188
226
  directory,
189
227
  manifest: fetched.document,
@@ -196,13 +234,25 @@ export async function readImageSourceEvidenceArtifact({
196
234
  }
197
235
  }
198
236
 
199
- /** Resolve the newest valid evidence referrer deterministically. */
237
+ /**
238
+ * Resolve the newest valid evidence referrer deterministically. A referrer
239
+ * stated under a different kernel contract authority is not evidence under
240
+ * `expectedContractAuthority`: it is reported to `onForeignAuthorityReferrer`
241
+ * and skipped. Every other defect of a referrer still throws.
242
+ */
200
243
  export async function resolveImageSourceEvidenceReferrer({
201
244
  client,
202
245
  imageRepository,
203
246
  subjectDigest,
204
247
  schemas,
248
+ expectedContractAuthority,
249
+ onForeignAuthorityReferrer,
205
250
  }) {
251
+ const expectedAuthorityCommit = expectedContractAuthority?.sourceCommit;
252
+ assertExpectedAuthorityCommit(expectedAuthorityCommit);
253
+ if (typeof onForeignAuthorityReferrer !== 'function') {
254
+ throw new Error('a foreign-authority referrer report handler is required');
255
+ }
206
256
  const { authority, repository } = splitImageRef(imageRepository);
207
257
  if (client.authority !== authority) {
208
258
  throw new Error(
@@ -224,16 +274,29 @@ export async function resolveImageSourceEvidenceReferrer({
224
274
  if (!SHA256_DIGEST_PATTERN.test(descriptor.digest ?? '')) {
225
275
  throw new Error('registry returned a non-canonical evidence referrer digest');
226
276
  }
227
- candidates.push(
228
- await readImageSourceEvidenceArtifact({
229
- client,
230
- repository,
277
+ try {
278
+ candidates.push(
279
+ await readImageSourceEvidenceArtifact({
280
+ client,
281
+ repository,
282
+ subjectDigest,
283
+ artifactDigest: descriptor.digest,
284
+ schemas,
285
+ expectedAuthorityCommit,
286
+ }),
287
+ );
288
+ } catch (error) {
289
+ if (!(error instanceof ForeignAuthorityReferrerError)) {throw error;}
290
+ onForeignAuthorityReferrer({
291
+ imageRepository,
231
292
  subjectDigest,
232
- artifactDigest: descriptor.digest,
233
- schemas,
234
- }),
235
- );
293
+ artifactDigest: error.artifactDigest,
294
+ sourceCommit: error.sourceCommit,
295
+ expectedAuthorityCommit,
296
+ });
297
+ }
236
298
  }
299
+ if (candidates.length === 0) {return null;}
237
300
  candidates.sort((left, right) => {
238
301
  const byCreated = compareStrings(right.created, left.created);
239
302
  return byCreated || compareStrings(left.artifactDigest, right.artifactDigest);
@@ -73,6 +73,7 @@ import { compareStrings, loadKernelDistributionSchemas } from './lib.mjs';
73
73
  import {
74
74
  createRegistryClientResolver,
75
75
  resolveImageSourceEvidenceReferrer,
76
+ writeForeignAuthorityReferrerReport,
76
77
  } from './oci-image-source-evidence.mjs';
77
78
  import {
78
79
  loadProducerKernelSchemas,
@@ -193,6 +194,8 @@ export async function planImageEvidenceRefresh({
193
194
  imageRepository: subject.imageRef,
194
195
  subjectDigest: image.digest,
195
196
  schemas,
197
+ expectedContractAuthority,
198
+ onForeignAuthorityReferrer: writeForeignAuthorityReferrerReport,
196
199
  });
197
200
  if (artifact !== null) {
198
201
  matches = artifact.records.filter(
@@ -62,6 +62,37 @@ export function producerStatementMatchesInput(statement, expectedStatement) {
62
62
  );
63
63
  }
64
64
 
65
+ /**
66
+ * The RAW `predicate.contractAuthority.sourceCommit` of every statement a
67
+ * producer evidence directory carries, read BEFORE any schema parse: the parse
68
+ * is bound to one kernel authority, so deciding whose authority a statement
69
+ * speaks under cannot go through it. A statement that states no string commit
70
+ * contributes nothing here; the strict parse refuses it later.
71
+ */
72
+ export function readRawStatementAuthorityCommits({ directory }) {
73
+ const index = JSON.parse(
74
+ decodeExactUtf8(
75
+ readBounded(directory, PRODUCER_EVIDENCE_INDEX_FILE, 'producer evidence index'),
76
+ 'producer evidence index',
77
+ ),
78
+ );
79
+ if (!Array.isArray(index?.producerEvidence)) {
80
+ throw new Error('producer evidence index is not non-empty schemaVersion 1');
81
+ }
82
+ const commits = new Set();
83
+ for (const entry of index.producerEvidence) {
84
+ const statement = JSON.parse(
85
+ decodeExactUtf8(
86
+ readBounded(directory, entry?.evidence?.statementPath, 'producer statement'),
87
+ 'producer statement',
88
+ ),
89
+ );
90
+ const commit = statement?.predicate?.contractAuthority?.sourceCommit;
91
+ if (typeof commit === 'string') {commits.add(commit);}
92
+ }
93
+ return commits;
94
+ }
95
+
65
96
  /** Read and structurally validate one producer evidence directory. */
66
97
  export function readProducerEvidenceDirectory({ directory, schemas }) {
67
98
  if (!isAbsolute(directory)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xemahq/xema",
3
- "version": "0.44.1",
3
+ "version": "0.45.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/xema-dev/xema-cli.git",
@@ -54,7 +54,7 @@
54
54
  "@xemahq/distribution-source-hash": "^0.2.2",
55
55
  "@xemahq/dsl": "^0.24.0",
56
56
  "@xemahq/identity-client": "^0.16.0",
57
- "@xemahq/contracts": "^0.94.0",
57
+ "@xemahq/contracts": "^0.102.0",
58
58
  "@xemahq/kernel-state-runtime": "^0.8.0",
59
59
  "@xemahq/license-verifier": "^0.2.0",
60
60
  "@xemahq/service-registry-nest": "^0.12.2",
@@ -92,6 +92,6 @@
92
92
  "typecheck": "tsc --noEmit"
93
93
  },
94
94
  "xemaRelease": {
95
- "sourceDigest": "sha256:68b1703a9457c36f912b5d8e3793d727b2e90e84034007bdadb6bbacbedd04e3"
95
+ "sourceDigest": "sha256:04abcd3c081c6cd50d0133c543310f75801357071b18834ec4b15af587760461"
96
96
  }
97
97
  }