@aikdna/kdna-core 0.20.0 → 0.22.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.
@@ -18,7 +18,9 @@
18
18
  * - mimetype must be the first entry in a .kdna container
19
19
  * - mimetype must be STORED (compression method 0) in a .kdna container
20
20
  * - the source directory must contain mimetype, kdna.json, payload.kdnab
21
- * - checksums.json and signatures/ are optional
21
+ * - checksums.json, attachments/, and signature.kdsig are optional
22
+ * - signature.kdsig is the RFC-0021 M1 `kdsig.ed25519` bundle; when present
23
+ * it must verify fail-closed against the canonical content digest
22
24
  * - lineage must be a single object (not an array)
23
25
  * - pack output is reproducible within one pinned toolchain/compressor;
24
26
  * DEFLATE bytes may differ across compressors, zlib versions, or systems
@@ -51,17 +53,46 @@ const {
51
53
  isVerifiedExternalEntitlement,
52
54
  } = require('../external-key-grant');
53
55
 
56
+ function warnDispatcherDegraded(e) {
57
+ process.emitWarning(
58
+ `container-dispatcher failed to load; falling back to the legacy reader: ${e.message}`,
59
+ { code: 'KDNA_DISPATCHER_DEGRADED' },
60
+ );
61
+ return { readAsset: null };
62
+ }
63
+
54
64
  const { readAsset } = (() => {
65
+ const dispatcherRequest = '../container-dispatcher.js';
66
+ // Absence check via require.resolve: resolution only inspects the module
67
+ // path and never executes the target, so it can only fail with
68
+ // MODULE_NOT_FOUND when the dispatcher file itself cannot be located.
69
+ // Message matching is intentionally not used: Node prints the require
70
+ // stack inside MODULE_NOT_FOUND messages, so a dispatcher whose own
71
+ // internal dependency is missing would mention "container-dispatcher"
72
+ // and be misclassified as absent.
55
73
  try {
56
- return require('../container-dispatcher.js');
57
- } catch {
58
- return { readAsset: null };
74
+ require.resolve(dispatcherRequest);
75
+ } catch (e) {
76
+ if (e && e.code === 'MODULE_NOT_FOUND') {
77
+ // The dispatcher file is genuinely absent: expected in minimal
78
+ // installs. Fall back silently.
79
+ return { readAsset: null };
80
+ }
81
+ return warnDispatcherDegraded(e);
82
+ }
83
+ // The file resolved; any failure from here on (missing internal
84
+ // dependency, syntax error, initialization throw, permission error) is a
85
+ // real load failure and must be surfaced.
86
+ try {
87
+ return require(dispatcherRequest);
88
+ } catch (e) {
89
+ return warnDispatcherDegraded(e);
59
90
  }
60
91
  })();
61
92
 
62
93
  const MIMETYPE = 'application/vnd.kdna.asset';
63
94
  const REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
64
- const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'signatures', 'attachments'];
95
+ const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'attachments', 'signature.kdsig'];
65
96
  const ALLOWED_TOP_LEVEL_ENTRIES = new Set([
66
97
  ...REQUIRED_DIR_ENTRIES,
67
98
  ...OPTIONAL_DIR_ENTRIES,
@@ -405,7 +436,7 @@ function validateContainerEntryMetadata(entry) {
405
436
  }
406
437
  throw new Error(`container includes unsupported top-level entry: ${topLevel}`);
407
438
  }
408
- if ((topLevel === 'signatures' || topLevel === 'attachments') && normalizedName === topLevel) {
439
+ if (topLevel === 'attachments' && normalizedName === topLevel) {
409
440
  throw new Error(`container directory entry is not supported: ${name}`);
410
441
  }
411
442
  if (method !== 0 && method !== 8) {
@@ -557,8 +588,15 @@ function readLayout(absPath) {
557
588
  if (fs.existsSync(full)) {
558
589
  if (fs.statSync(full).isFile()) {
559
590
  map[f] = fs.readFileSync(full);
591
+ } else if (f === 'attachments') {
592
+ // Attachment files join the entry map under their relative names so
593
+ // digest and signature operations see the same entry set as the
594
+ // packaged container.
595
+ for (const entry of listSourceDir(full)) {
596
+ map[`attachments/${entry.rel}`] = fs.readFileSync(entry.full);
597
+ }
560
598
  } else {
561
- // subdirectory like signatures/ — record its presence but not contents here
599
+ // Optional subdirectories are recorded without becoming payloads.
562
600
  map[f] = null;
563
601
  }
564
602
  }
@@ -570,13 +608,14 @@ function readLayout(absPath) {
570
608
  containerBytes = fs.readFileSync(absPath);
571
609
  entries = listZipEntriesFromBuffer(containerBytes);
572
610
  for (const e of entries) {
573
- // We only need the well-known entries; signatures/ attachments/ etc.
611
+ // We only need the well-known entries and optional attachments.
574
612
  // are passed through unchanged by the loader but not parsed here.
575
613
  if (
576
614
  e.name === 'mimetype' ||
577
615
  e.name === 'kdna.json' ||
578
616
  e.name === 'payload.kdnab' ||
579
- e.name === 'checksums.json'
617
+ e.name === 'checksums.json' ||
618
+ e.name === 'signature.kdsig'
580
619
  ) {
581
620
  map[e.name] = e.data;
582
621
  }
@@ -633,7 +672,8 @@ function readLayoutBytes(input) {
633
672
  entry.name === 'mimetype' ||
634
673
  entry.name === 'kdna.json' ||
635
674
  entry.name === 'payload.kdnab' ||
636
- entry.name === 'checksums.json'
675
+ entry.name === 'checksums.json' ||
676
+ entry.name === 'signature.kdsig'
637
677
  ) {
638
678
  map[entry.name] = entry.data;
639
679
  }
@@ -732,6 +772,32 @@ function parsePayloadEntry(name, bytes, opts = {}) {
732
772
  return parsed;
733
773
  }
734
774
 
775
+ function assertDecryptedPayloadValid(payload, manifest) {
776
+ const validators = loadSchemas();
777
+ if (!validators) {
778
+ const error = new Error(
779
+ 'Decrypted payload schema validation is unavailable.',
780
+ );
781
+ error.code = 'KDNA_PAYLOAD_SCHEMA_UNAVAILABLE';
782
+ throw error;
783
+ }
784
+ const isBundleProfile =
785
+ manifest.compatibility?.profile === 'kdna.payload.bundle' ||
786
+ manifest.asset_type === 'bundle';
787
+ const validator = isBundleProfile
788
+ ? validators.bundlePayload
789
+ : validators.payload;
790
+ if (validator(payload)) return payload;
791
+ const details = (validator.errors || [])
792
+ .map((entry) => `${entry.instancePath || '<root>'} ${entry.message}`)
793
+ .join('; ');
794
+ const error = new Error(
795
+ `Decrypted payload failed its declared schema${details ? `: ${details}` : '.'}`,
796
+ );
797
+ error.code = 'KDNA_PAYLOAD_INVALID';
798
+ throw error;
799
+ }
800
+
735
801
  // ─── inspect ───────────────────────────────────────────────────────────
736
802
 
737
803
  /**
@@ -754,8 +820,8 @@ function buildInspectOutput(container) {
754
820
  ...assessLoaderCompatibility(m),
755
821
  load_contract_default_profile: m.load_contract ? m.load_contract.default_profile : null,
756
822
  };
757
- if (m.signatures !== undefined) out.signature_count = Array.isArray(m.signatures) ? m.signatures.length : 0;
758
823
  if (container.map['checksums.json']) out.checksums_present = true;
824
+ if (container.map['signature.kdsig']) out.signature_present = true;
759
825
  return out;
760
826
  }
761
827
 
@@ -772,6 +838,9 @@ function runValidate(layout) {
772
838
  schema_valid: true,
773
839
  payload_valid: true,
774
840
  checksums_valid: true,
841
+ signature_valid: true,
842
+ signature_state: 'absent',
843
+ signature_evidence: null,
775
844
  load_contract_valid: true,
776
845
  ...assessLoaderCompatibility(layout.manifest),
777
846
  };
@@ -926,6 +995,26 @@ function runValidate(layout) {
926
995
  }
927
996
  }
928
997
 
998
+ // signature gate — RFC-0021 M1 (`kdsig.ed25519`). An absent signature is
999
+ // not an error; a present signature must verify fail-closed.
1000
+ if (layout.map['signature.kdsig'] !== undefined) {
1001
+ try {
1002
+ const signature = require('../signature');
1003
+ const { contentDigestFromEntryBuffers } = require('../asset-reader');
1004
+ const contentDigest = contentDigestFromEntryBuffers(fullEntryBufferMap(layout));
1005
+ const evidence = signature.verifySignatureBundle(
1006
+ layout.map['signature.kdsig'],
1007
+ contentDigest,
1008
+ );
1009
+ result.signature_state = 'verified';
1010
+ result.signature_evidence = evidence;
1011
+ } catch (e) {
1012
+ result.signature_valid = false;
1013
+ result.signature_state = 'invalid';
1014
+ problems.push(`signature: ${e.message}`);
1015
+ }
1016
+ }
1017
+
929
1018
  // load_contract gate — only if manifest references a load_contract block
930
1019
  if (layout.manifest.load_contract) {
931
1020
  const lc = layout.manifest.load_contract;
@@ -1043,12 +1132,39 @@ function verifyDigests(checksums, map, problems, result) {
1043
1132
  return stillValid;
1044
1133
  }
1045
1134
 
1135
+ /**
1136
+ * Collect every file entry of a layout as name → Buffer, so digest and
1137
+ * signature operations always cover the full entry set (including
1138
+ * attachments), regardless of which entries layout.map parses.
1139
+ */
1140
+ function fullEntryBufferMap(layout) {
1141
+ const buffers = {};
1142
+ if (Array.isArray(layout.entries)) {
1143
+ for (const entry of layout.entries) {
1144
+ // Entries are either parsed ZIP entry objects ({ name, data }) or plain
1145
+ // entry names from the unified container model; both resolve through
1146
+ // layout.map.
1147
+ const name = typeof entry === 'string' ? entry : entry.name;
1148
+ const data = typeof entry === 'string' ? layout.map[entry] : entry.data;
1149
+ if (data === undefined || data === null) continue;
1150
+ buffers[name] = data;
1151
+ }
1152
+ return buffers;
1153
+ }
1154
+ for (const [name, data] of Object.entries(layout.map)) {
1155
+ if (data === null || data === undefined) continue;
1156
+ buffers[name] = data;
1157
+ }
1158
+ return buffers;
1159
+ }
1160
+
1046
1161
  function finalizeValidate(result, problems) {
1047
1162
  result.overall_valid =
1048
1163
  result.format_valid &&
1049
1164
  result.schema_valid &&
1050
1165
  result.payload_valid &&
1051
1166
  result.checksums_valid &&
1167
+ result.signature_valid &&
1052
1168
  result.load_contract_valid;
1053
1169
  result.problems = problems;
1054
1170
  return result;
@@ -1205,6 +1321,108 @@ function pack(sourceDir, outputPath) {
1205
1321
  return { outputPath, entries: order };
1206
1322
  }
1207
1323
 
1324
+ /**
1325
+ * Deterministically pack an in-memory entry map into .kdna container bytes.
1326
+ * mimetype is forced first and STORED; remaining entries are ordered by UTF-8
1327
+ * path bytes and DEFLATE-compressed; timestamps are pinned to the DOS epoch.
1328
+ */
1329
+ function packEntryMap(map) {
1330
+ for (const f of REQUIRED_DIR_ENTRIES) {
1331
+ if (!map[f]) throw new Error(`cannot pack: missing required entry ${f}`);
1332
+ }
1333
+ const mime = Buffer.isBuffer(map.mimetype) ? map.mimetype.toString('utf8') : String(map.mimetype);
1334
+ if (mime !== MIMETYPE) {
1335
+ throw new Error(`cannot pack: mimetype is "${mime}", expected "${MIMETYPE}"`);
1336
+ }
1337
+ const names = Object.keys(map).filter((name) => name !== 'mimetype');
1338
+ names.sort((a, b) => Buffer.compare(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8')));
1339
+ const order = ['mimetype', ...names];
1340
+
1341
+ const localChunks = [];
1342
+ const centralChunks = [];
1343
+ let offset = 0;
1344
+ for (const rel of order) {
1345
+ const data = rel === 'mimetype' ? Buffer.from(MIMETYPE, 'utf8') : Buffer.from(map[rel]);
1346
+ const nameBytes = Buffer.from(rel, 'utf8');
1347
+ const method = rel === 'mimetype' ? 0 : 8;
1348
+ const built = buildLocalHeader(nameBytes, data, method);
1349
+ localChunks.push(built.local, built.compressed);
1350
+ centralChunks.push(
1351
+ buildCentral(
1352
+ {
1353
+ method,
1354
+ crc: built.crc,
1355
+ time: built.time,
1356
+ date: built.date,
1357
+ compressed: built.compressed,
1358
+ dataLength: built.dataLength,
1359
+ offset,
1360
+ },
1361
+ nameBytes,
1362
+ ),
1363
+ );
1364
+ offset += built.local.length + built.compressed.length;
1365
+ }
1366
+
1367
+ const centralOffset = offset;
1368
+ let centralSize = 0;
1369
+ for (const c of centralChunks) centralSize += c.length;
1370
+ const eocd = Buffer.alloc(22);
1371
+ eocd.writeUInt32LE(0x06054b50, 0);
1372
+ eocd.writeUInt16LE(0, 4);
1373
+ eocd.writeUInt16LE(0, 6);
1374
+ eocd.writeUInt16LE(order.length, 8);
1375
+ eocd.writeUInt16LE(order.length, 10);
1376
+ eocd.writeUInt32LE(centralSize, 12);
1377
+ eocd.writeUInt32LE(centralOffset, 16);
1378
+ eocd.writeUInt16LE(0, 20);
1379
+
1380
+ return Buffer.concat([...localChunks, ...centralChunks, eocd]);
1381
+ }
1382
+
1383
+ /**
1384
+ * Sign a packaged .kdna container per RFC-0021 M1 (`kdsig.ed25519`).
1385
+ *
1386
+ * The container must pass every validation gate after any pre-existing
1387
+ * signature entry is removed; signing never certifies a broken asset. The
1388
+ * returned bytes carry a fresh `signature.kdsig` bundle whose Ed25519
1389
+ * signature covers the canonical content digest. Fail-closed throughout.
1390
+ */
1391
+ function signContainerBytes(input, privateKeySeedHex, _opts = {}) {
1392
+ const signature = require('../signature');
1393
+ const { contentDigestFromEntryBuffers } = require('../asset-reader');
1394
+
1395
+ const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
1396
+ const layout = readLayoutBytes(bytes);
1397
+
1398
+ // Rebuild the full entry map (including attachments) from the parsed
1399
+ // ZIP entries; layout.map only carries the well-known protocol entries.
1400
+ const unsignedMap = {};
1401
+ for (const entry of layout.entries) {
1402
+ if (entry.name === 'signature.kdsig') continue;
1403
+ unsignedMap[entry.name] = entry.data;
1404
+ }
1405
+ const validation = runValidate({ ...layout, map: unsignedMap });
1406
+ if (!validation.overall_valid) {
1407
+ const error = new Error(
1408
+ `cannot sign: container does not pass validation (${validation.problems.join('; ')})`,
1409
+ );
1410
+ error.code = 'KDNA_SIGNATURE_INPUT_INVALID';
1411
+ throw error;
1412
+ }
1413
+
1414
+ const contentDigest = contentDigestFromEntryBuffers(unsignedMap);
1415
+ const bundle = signature.signContentDigest(contentDigest, privateKeySeedHex);
1416
+ const bundleBytes = signature.serializeSignatureBundle(bundle);
1417
+ const signedMap = { ...unsignedMap, [signature.SIGNATURE_ENTRY_NAME]: bundleBytes };
1418
+ return {
1419
+ containerBytes: packEntryMap(signedMap),
1420
+ bundle,
1421
+ bundleBytes,
1422
+ content_digest: contentDigest,
1423
+ };
1424
+ }
1425
+
1208
1426
  // ─── unpack ────────────────────────────────────────────────────────────
1209
1427
 
1210
1428
  /**
@@ -1285,6 +1503,12 @@ function buildLoadPlanIssue(code, severity, message) {
1285
1503
 
1286
1504
  function validationProblemCode(problem) {
1287
1505
  if (/checksums?:/i.test(problem)) return 'KDNA_INTEGRITY_DIGEST_FAILED';
1506
+ if (/signature bundle profile_version .* is not supported/i.test(problem)) {
1507
+ return 'KDNA_SIGNATURE_VERSION_UNSUPPORTED';
1508
+ }
1509
+ if (/signature bundle (profile|algorithm) .* is not supported/i.test(problem)) {
1510
+ return 'KDNA_SIGNATURE_PROFILE_UNSUPPORTED';
1511
+ }
1288
1512
  if (/signature/i.test(problem)) return 'KDNA_INTEGRITY_SIGNATURE_FAILED';
1289
1513
  if (/payload:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
1290
1514
  if (/manifest:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
@@ -1322,7 +1546,9 @@ function inputFingerprint(inputPath, layout, opts = {}) {
1322
1546
  ) ? opts.entitlement.status : null;
1323
1547
 
1324
1548
  return {
1325
- has_password_input: opts.hasPassword === true,
1549
+ has_password_input:
1550
+ opts.hasPassword === true ||
1551
+ (typeof opts.password === 'string' && opts.password.length > 0),
1326
1552
  entitlement_input: entitlementInput,
1327
1553
  source_fingerprint: computeSourceFingerprint(inputPath, layout),
1328
1554
  };
@@ -1356,9 +1582,11 @@ function baseLoadPlan(inputPath, layout, validation, opts = {}) {
1356
1582
  schema_valid: validation.schema_valid,
1357
1583
  payload_valid: validation.payload_valid,
1358
1584
  checksums_valid: validation.checksums_valid,
1585
+ signature_valid: validation.signature_valid,
1359
1586
  load_contract_valid: validation.load_contract_valid,
1360
1587
  overall_valid: validation.overall_valid,
1361
1588
  },
1589
+ signature_state: validation.signature_state,
1362
1590
  issues: [],
1363
1591
  source: {
1364
1592
  kind: layout.kind,
@@ -1424,7 +1652,8 @@ function planLoad(inputPath, opts = {}) {
1424
1652
  access: null, access_alias: null, entitlement_profile: null,
1425
1653
  state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
1426
1654
  input_fingerprint: null,
1427
- checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
1655
+ checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, signature_valid: false, load_contract_valid: false, overall_valid: false },
1656
+ signature_state: 'absent',
1428
1657
  issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
1429
1658
  source: inputSource(inputPath, 'memory'),
1430
1659
  });
@@ -1448,7 +1677,8 @@ function planLoad(inputPath, opts = {}) {
1448
1677
  access: null, access_alias: null, entitlement_profile: null,
1449
1678
  state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
1450
1679
  input_fingerprint: null,
1451
- checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
1680
+ checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, signature_valid: false, load_contract_valid: false, overall_valid: false },
1681
+ signature_state: 'absent',
1452
1682
  issues: [buildLoadPlanIssue(
1453
1683
  e.code === 'KDNA_FORMAT_UNKNOWN' ? 'KDNA_FORMAT_UNKNOWN' : 'KDNA_FORMAT_INVALID',
1454
1684
  'blocking', e.message,
@@ -1469,7 +1699,8 @@ function planLoad(inputPath, opts = {}) {
1469
1699
  access: null, access_alias: null, entitlement_profile: null,
1470
1700
  state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
1471
1701
  input_fingerprint: null,
1472
- checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
1702
+ checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, signature_valid: false, load_contract_valid: false, overall_valid: false },
1703
+ signature_state: 'absent',
1473
1704
  issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
1474
1705
  source: inputSource(inputPath, null),
1475
1706
  });
@@ -1675,18 +1906,19 @@ function planLoad(inputPath, opts = {}) {
1675
1906
  }
1676
1907
 
1677
1908
  if (plan.entitlement_profile === 'password') {
1678
- if (opts.password || opts.hasPassword === true) {
1679
- if (opts.hasPassword === true && !opts.password) {
1680
- plan.issues.push(buildLoadPlanIssue(
1681
- 'KDNA_AUTH_PASSWORD_DIAGNOSTIC',
1682
- 'info',
1683
- 'hasPassword is a diagnostic credential-presence signal only; it does not verify the password.',
1684
- ));
1685
- }
1686
- plan.state = 'ready';
1687
- plan.required_action = 'load';
1688
- plan.can_load_now = true;
1689
- plan.projection_policy = 'minimal';
1909
+ if (
1910
+ opts.hasPassword === true ||
1911
+ (typeof opts.password === 'string' && opts.password.length > 0)
1912
+ ) {
1913
+ plan.state = 'needs_password';
1914
+ plan.required_action = 'enter_password';
1915
+ plan.can_load_now = false;
1916
+ plan.projection_policy = 'none';
1917
+ plan.issues.push(buildLoadPlanIssue(
1918
+ 'KDNA_AUTH_PASSWORD_UNVERIFIED',
1919
+ 'blocking',
1920
+ 'A password was provided but has not been verified. Only an authorized load may verify it by decrypting the protected payload.',
1921
+ ));
1690
1922
  } else {
1691
1923
  plan.state = 'needs_password';
1692
1924
  plan.required_action = 'enter_password';
@@ -1921,6 +2153,8 @@ module.exports = {
1921
2153
  isKdnaSourceDir,
1922
2154
  detectContainerFormat,
1923
2155
  readLayout: readLayout,
2156
+ readLayoutBytes,
2157
+ fullEntryBufferMap,
1924
2158
  inspect,
1925
2159
  validate,
1926
2160
  planLoad,
@@ -1928,6 +2162,8 @@ module.exports = {
1928
2162
  buildChecksums: buildChecksums,
1929
2163
  computeRuntimeEntrySetDigest,
1930
2164
  pack,
2165
+ packEntryMap,
2166
+ signContainerBytes,
1931
2167
  unpack,
1932
2168
  load: loadAsset,
1933
2169
  loadAsset: loadAsset,
@@ -1958,10 +2194,10 @@ function renderPromptItem(item) {
1958
2194
  if (item.type === 'axiom_applicability' && item.one_sentence) {
1959
2195
  const parts = [item.one_sentence];
1960
2196
  if (Array.isArray(item.applies_when) && item.applies_when.length) {
1961
- parts.push(`applies when: ${item.applies_when.slice(0, 2).join('; ')}`);
2197
+ parts.push(`applies when: ${item.applies_when.join('; ')}`);
1962
2198
  }
1963
2199
  if (Array.isArray(item.does_not_apply_when) && item.does_not_apply_when.length) {
1964
- parts.push(`does not apply when: ${item.does_not_apply_when.slice(0, 2).join('; ')}`);
2200
+ parts.push(`does not apply when: ${item.does_not_apply_when.join('; ')}`);
1965
2201
  }
1966
2202
  if (item.failure_risk) parts.push(`failure risk: ${item.failure_risk}`);
1967
2203
  return parts.join(' — ');
@@ -2035,7 +2271,13 @@ function loadAuthorized(inputPath, opts = {}) {
2035
2271
  const inputKind = typeof inputPath === 'string' ? 'packaged_file' : 'packaged_bytes';
2036
2272
  const inputSnapshot = snapshotPackagedInput(inputPath);
2037
2273
  const plan = planLoad(inputSnapshot, opts);
2038
- if (plan.can_load_now !== true) {
2274
+ const loadMayVerifyPassword =
2275
+ typeof opts.password === 'string' &&
2276
+ opts.password.length > 0 &&
2277
+ plan.state === 'needs_password' &&
2278
+ Array.isArray(plan.issues) &&
2279
+ plan.issues.some((issue) => issue.code === 'KDNA_AUTH_PASSWORD_UNVERIFIED');
2280
+ if (plan.can_load_now !== true && !loadMayVerifyPassword) {
2039
2281
  const issueCodes = Array.isArray(plan.issues)
2040
2282
  ? plan.issues.map((issue) => issue.code).filter(Boolean)
2041
2283
  : [];
@@ -2052,7 +2294,8 @@ function loadAuthorized(inputPath, opts = {}) {
2052
2294
  extendsChain: plan.extends_chain || [],
2053
2295
  _validation: {
2054
2296
  schema_valid: plan.checks && plan.checks.overall_valid === true,
2055
- signature_valid: plan.signature_valid === true,
2297
+ signature_valid: plan.checks && plan.checks.signature_valid === true,
2298
+ signature_state: plan.signature_state,
2056
2299
  },
2057
2300
  _runtimeAssetBytes: inputSnapshot,
2058
2301
  _runtimeInputKind: inputKind,
@@ -2094,9 +2337,11 @@ function packagedAssetRequiredPlan(inputPath) {
2094
2337
  schema_valid: false,
2095
2338
  payload_valid: false,
2096
2339
  checksums_valid: false,
2340
+ signature_valid: false,
2097
2341
  load_contract_valid: false,
2098
2342
  overall_valid: false,
2099
2343
  },
2344
+ signature_state: 'absent',
2100
2345
  issues: [buildLoadPlanIssue(
2101
2346
  'KDNA_ASSET_FILE_REQUIRED',
2102
2347
  'blocking',
@@ -2195,7 +2440,8 @@ function loadRemoteRuntimeAssetForServer(inputPath) {
2195
2440
  extendsChain: [],
2196
2441
  _validation: {
2197
2442
  schema_valid: plan.checks.overall_valid === true,
2198
- signature_valid: plan.signature_valid === true,
2443
+ signature_valid: plan.checks.signature_valid === true,
2444
+ signature_state: plan.signature_state,
2199
2445
  },
2200
2446
  _runtimeAssetBytes: inputSnapshot,
2201
2447
  _runtimeInputKind: inputKind,
@@ -2226,7 +2472,11 @@ function normalizeCompactAxiom(axiom) {
2226
2472
  if (!axiom || typeof axiom !== 'object') return null;
2227
2473
  const statement = axiom.statement || axiom.one_sentence || axiom.full_statement || axiom.id || null;
2228
2474
  if (!statement) return null;
2229
- const oneSentence = (axiom.one_sentence && !String(axiom.one_sentence).startsWith('<TBD')) ? axiom.one_sentence : (typeof axiom.full_statement === 'string' && axiom.full_statement.length > 0 ? axiom.full_statement.substring(0, 120) + (axiom.full_statement.length > 120 ? '…' : '') : statement);
2475
+ const oneSentence = (axiom.one_sentence && !String(axiom.one_sentence).startsWith('<TBD'))
2476
+ ? axiom.one_sentence
2477
+ : (typeof axiom.full_statement === 'string' && axiom.full_statement.length > 0
2478
+ ? axiom.full_statement
2479
+ : statement);
2230
2480
  return {
2231
2481
  type: 'axiom_applicability',
2232
2482
  id: axiom.id || null,
@@ -2251,6 +2501,27 @@ function hasStandardJudgmentRoleContent(role) {
2251
2501
  );
2252
2502
  }
2253
2503
 
2504
+ function projectMinimalAxiom(axiom) {
2505
+ const normalized = normalizeCompactAxiom(axiom);
2506
+ if (!normalized) return null;
2507
+ return {
2508
+ type: 'axiom_applicability',
2509
+ id: normalized.id || null,
2510
+ one_sentence: normalized.one_sentence || null,
2511
+ does_not_apply_when: normalized.does_not_apply_when || [],
2512
+ failure_risk: normalized.failure_risk || null,
2513
+ };
2514
+ }
2515
+
2516
+ function hasMinimalPromptContent(content) {
2517
+ if (!content) return false;
2518
+ return Boolean(
2519
+ content.highest_question ||
2520
+ (Array.isArray(content.axioms) && content.axioms.length > 0) ||
2521
+ (Array.isArray(content.boundaries) && content.boundaries.length > 0)
2522
+ );
2523
+ }
2524
+
2254
2525
  function hasCompactPromptContent(content) {
2255
2526
  if (!content) return false;
2256
2527
  return Boolean(
@@ -2260,12 +2531,120 @@ function hasCompactPromptContent(content) {
2260
2531
  hasStandardJudgmentRoleContent(content.judgment_role) ||
2261
2532
  (Array.isArray(content.axioms) && content.axioms.length > 0) ||
2262
2533
  (Array.isArray(content.boundaries) && content.boundaries.length > 0) ||
2534
+ (Array.isArray(content.core_structure) && content.core_structure.length > 0) ||
2263
2535
  (Array.isArray(content.self_checks) && content.self_checks.length > 0) ||
2264
2536
  (Array.isArray(content.failure_modes) && content.failure_modes.length > 0) ||
2265
2537
  (Array.isArray(content.patterns) && content.patterns.length > 0)
2266
2538
  );
2267
2539
  }
2268
2540
 
2541
+ const COMPACT_PROJECTED_CORE_FIELDS = new Set([
2542
+ 'highest_question',
2543
+ 'worldview',
2544
+ 'value_order',
2545
+ 'judgment_role',
2546
+ 'axioms',
2547
+ 'boundaries',
2548
+ 'core_structure',
2549
+ ]);
2550
+ const COMPACT_PROJECTED_REASONING_FIELDS = new Set(['self_check', 'failure_modes']);
2551
+ const COMPACT_PROJECTED_AXIOM_FIELDS = new Set([
2552
+ 'id',
2553
+ 'statement',
2554
+ 'one_sentence',
2555
+ 'applies_when',
2556
+ 'does_not_apply_when',
2557
+ 'failure_risk',
2558
+ ]);
2559
+ const COMPACT_NON_CONTENT_FIELDS = new Set(['profile', 'profile_version']);
2560
+
2561
+ function escapeJsonPointerToken(value) {
2562
+ return String(value).replaceAll('~', '~0').replaceAll('/', '~1');
2563
+ }
2564
+
2565
+ function omissionCount(value) {
2566
+ if (value === undefined || value === null) return 0;
2567
+ if (Array.isArray(value)) return value.length;
2568
+ if (typeof value === 'string') return value.trim() ? 1 : 0;
2569
+ if (typeof value === 'object') return Object.keys(value).length > 0 ? 1 : 0;
2570
+ return 1;
2571
+ }
2572
+
2573
+ function addOmittedValue(entries, pointer, value) {
2574
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
2575
+ for (const key of Object.keys(value).sort()) {
2576
+ addOmittedValue(
2577
+ entries,
2578
+ `${pointer}/${escapeJsonPointerToken(key)}`,
2579
+ value[key],
2580
+ );
2581
+ }
2582
+ return;
2583
+ }
2584
+ const count = omissionCount(value);
2585
+ if (count > 0) entries.push({ path: pointer, count });
2586
+ }
2587
+
2588
+ function addOmittedAxiomFields(entries, axioms) {
2589
+ const counts = new Map();
2590
+ for (const axiom of Array.isArray(axioms) ? axioms : []) {
2591
+ if (!axiom || typeof axiom !== 'object' || Array.isArray(axiom)) continue;
2592
+ for (const key of Object.keys(axiom).sort()) {
2593
+ if (COMPACT_PROJECTED_AXIOM_FIELDS.has(key)) continue;
2594
+ const count = omissionCount(axiom[key]);
2595
+ if (count === 0) continue;
2596
+ const path = `/core/axioms/*/${escapeJsonPointerToken(key)}`;
2597
+ counts.set(path, (counts.get(path) || 0) + count);
2598
+ }
2599
+ }
2600
+ for (const path of [...counts.keys()].sort()) {
2601
+ entries.push({ path, count: counts.get(path) });
2602
+ }
2603
+ }
2604
+
2605
+ function buildCompactProjectionReport(payload) {
2606
+ const entries = [];
2607
+ const core = payload && typeof payload.core === 'object' && !Array.isArray(payload.core)
2608
+ ? payload.core
2609
+ : {};
2610
+ const reasoning = payload && typeof payload.reasoning === 'object' && !Array.isArray(payload.reasoning)
2611
+ ? payload.reasoning
2612
+ : {};
2613
+
2614
+ for (const key of Object.keys(core).sort()) {
2615
+ if (!COMPACT_PROJECTED_CORE_FIELDS.has(key)) {
2616
+ addOmittedValue(entries, `/core/${escapeJsonPointerToken(key)}`, core[key]);
2617
+ }
2618
+ }
2619
+ addOmittedAxiomFields(entries, core.axioms);
2620
+
2621
+ for (const key of Object.keys(reasoning).sort()) {
2622
+ if (!COMPACT_PROJECTED_REASONING_FIELDS.has(key)) {
2623
+ addOmittedValue(entries, `/reasoning/${escapeJsonPointerToken(key)}`, reasoning[key]);
2624
+ }
2625
+ }
2626
+
2627
+ for (const key of Object.keys(payload || {}).sort()) {
2628
+ if (
2629
+ COMPACT_NON_CONTENT_FIELDS.has(key) ||
2630
+ key === 'core' ||
2631
+ key === 'patterns' ||
2632
+ key === 'reasoning'
2633
+ ) {
2634
+ continue;
2635
+ }
2636
+ addOmittedValue(entries, `/${escapeJsonPointerToken(key)}`, payload[key]);
2637
+ }
2638
+
2639
+ entries.sort((left, right) => left.path.localeCompare(right.path));
2640
+ const omittedTotal = entries.reduce((total, entry) => total + entry.count, 0);
2641
+ return {
2642
+ status: omittedTotal > 0 ? 'partial' : 'complete',
2643
+ omitted: entries,
2644
+ omitted_total: omittedTotal,
2645
+ };
2646
+ }
2647
+
2269
2648
  function loadAssetUnsafe(inputPath, opts = {}) {
2270
2649
  const layout = readInputLayout(inputPath);
2271
2650
  const m = layout.manifest;
@@ -2284,6 +2663,7 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2284
2663
  // B4: Attempt decryption when password or decrypt hook is available.
2285
2664
  if (opts.decryptEntry) {
2286
2665
  // Consumer-provided decrypt hook (e.g., licensed entry key)
2666
+ let decryptedBuf;
2287
2667
  try {
2288
2668
  const decrypted = opts.decryptEntry({
2289
2669
  asset: { entries: layout.map, manifest: m },
@@ -2291,18 +2671,27 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2291
2671
  entryName: 'payload.kdnab',
2292
2672
  ciphertext: layout.map['payload.kdnab'],
2293
2673
  });
2294
- const decryptedBuf = typeof decrypted === 'string'
2674
+ decryptedBuf = typeof decrypted === 'string'
2295
2675
  ? Buffer.from(decrypted, 'utf8')
2296
2676
  : Buffer.from(decrypted);
2297
- payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
2298
2677
  } catch (e) {
2299
2678
  const err = new Error(`Decryption failed: ${e.message}`);
2300
2679
  err.code = e.code || 'KDNA_DECRYPT_FAILED';
2301
2680
  throw err;
2302
2681
  }
2682
+ try {
2683
+ payload = assertDecryptedPayloadValid(
2684
+ parsePayloadEntry('payload.kdnab', decryptedBuf),
2685
+ m,
2686
+ );
2687
+ } catch (error) {
2688
+ if (!error.code) error.code = 'KDNA_PAYLOAD_INVALID';
2689
+ throw error;
2690
+ }
2303
2691
  } else if (opts.password) {
2304
2692
  // Password-based decryption via kdna.encryption.password*
2305
2693
  // profiles. Detects Argon2id vs scrypt from the envelope profile.
2694
+ let decryptedBuf;
2306
2695
  try {
2307
2696
  const {
2308
2697
  decryptProtectedEntry,
@@ -2316,17 +2705,25 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2316
2705
  const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
2317
2706
  ? decryptProtectedEntryScrypt
2318
2707
  : decryptProtectedEntry;
2319
- const decryptedBuf = decryptFn(encryptedEnvelope, {
2708
+ decryptedBuf = decryptFn(encryptedEnvelope, {
2320
2709
  entryName: 'payload.kdnab',
2321
2710
  manifest: m,
2322
2711
  password: opts.password,
2323
2712
  });
2324
- payload = parsePayloadEntry('payload.kdnab', decryptedBuf);
2325
2713
  } catch (e) {
2326
2714
  const err = new Error(`Decryption failed with provided password: ${e.message}`);
2327
2715
  err.code = 'KDNA_DECRYPT_FAILED';
2328
2716
  throw err;
2329
2717
  }
2718
+ try {
2719
+ payload = assertDecryptedPayloadValid(
2720
+ parsePayloadEntry('payload.kdnab', decryptedBuf),
2721
+ m,
2722
+ );
2723
+ } catch (error) {
2724
+ if (!error.code) error.code = 'KDNA_PAYLOAD_INVALID';
2725
+ throw error;
2726
+ }
2330
2727
  } else {
2331
2728
  const isExternal = payload.profile === EXTERNAL_ENVELOPE_PROFILE;
2332
2729
  const err = new Error(
@@ -2372,6 +2769,17 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2372
2769
  || (payload.reasoning && ((payload.reasoning.self_check && payload.reasoning.self_check.length > 0) || (payload.reasoning.failure_modes && payload.reasoning.failure_modes.length > 0)));
2373
2770
  profiles.push('index');
2374
2771
  if (hasJudgment) profiles.push('compact');
2772
+ // RFC-0020: minimal availability follows the manifest declaration, not
2773
+ // payload inference. An asset that declares minimal in load_contract
2774
+ // (and carries core judgment) may be projected minimally.
2775
+ if (
2776
+ hasJudgment &&
2777
+ m.load_contract &&
2778
+ m.load_contract.profiles &&
2779
+ m.load_contract.profiles.minimal
2780
+ ) {
2781
+ profiles.push('minimal');
2782
+ }
2375
2783
  if (payload.scenarios && payload.scenarios.length > 0) profiles.push('scenario');
2376
2784
  profiles.push('full');
2377
2785
  return profiles;
@@ -2408,6 +2816,32 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2408
2816
  ))
2409
2817
  : []
2410
2818
  );
2819
+ const projectCoreStructure = (items) => (
2820
+ Array.isArray(items)
2821
+ ? items.map((relation) => {
2822
+ if (
2823
+ !relation ||
2824
+ typeof relation !== 'object' ||
2825
+ Array.isArray(relation)
2826
+ ) {
2827
+ return null;
2828
+ }
2829
+ const projected = {};
2830
+ for (const key of [
2831
+ 'from',
2832
+ 'to',
2833
+ 'via',
2834
+ 'applies_when',
2835
+ 'does_not_apply_when',
2836
+ ]) {
2837
+ if (relation[key] !== undefined) {
2838
+ projected[key] = globalThis.structuredClone(relation[key]);
2839
+ }
2840
+ }
2841
+ return projected;
2842
+ }).filter(Boolean)
2843
+ : []
2844
+ );
2411
2845
  result.content = {
2412
2846
  highest_question: core.highest_question || null,
2413
2847
  // These scoped domain-level values are already validated by the payload
@@ -2423,16 +2857,48 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2423
2857
  : null,
2424
2858
  axioms: (core.axioms || []).map(normalizeCompactAxiom).filter(Boolean),
2425
2859
  boundaries: normalizeList(core.boundaries),
2860
+ // Relations are already part of the public Payload. Preserve order and
2861
+ // the closed public relation fields, never arbitrary authoring state.
2862
+ core_structure: projectCoreStructure(core.core_structure),
2426
2863
  // Payload uses the singular field name while Runtime Capsule uses the
2427
2864
  // plural collection name. Preserve every validated item and its shape;
2428
2865
  // strings remain strings and structured questions remain objects.
2429
2866
  self_checks: preserveDeclaredList(payload.reasoning && payload.reasoning.self_check),
2430
2867
  failure_modes: normalizeList(payload.reasoning && payload.reasoning.failure_modes),
2431
- patterns: normalizeList(payload.patterns).slice(0, 3),
2868
+ patterns: normalizeList(payload.patterns),
2432
2869
  };
2870
+ result.projection_report = buildCompactProjectionReport(payload);
2433
2871
  if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.compact && m.load_contract.profiles.compact.max_tokens_hint) {
2434
2872
  result.max_tokens_hint = m.load_contract.profiles.compact.max_tokens_hint;
2435
2873
  }
2874
+ } else if (profile === 'minimal') {
2875
+ const core = payload.core || {};
2876
+ // RFC-0020: minimal availability follows the manifest declaration. If the
2877
+ // asset does not declare minimal in load_contract, fail closed with an
2878
+ // empty projection rather than silently serving a projection the asset
2879
+ // did not opt into.
2880
+ if (!result.profile_available) {
2881
+ result.content = {};
2882
+ result.projection_report = buildCompactProjectionReport(payload);
2883
+ } else {
2884
+ const normalizeList = (items) => (items || []).map((item) => {
2885
+ if (typeof item === 'string') return { type: 'text', text: item };
2886
+ if (item && typeof item === 'object') return item;
2887
+ return null;
2888
+ }).filter(Boolean);
2889
+ // RFC-0020: minimal projects the core judgment surface a small model needs:
2890
+ // highest_question + boundary-friendly axioms + full boundaries. It is a
2891
+ // strict subset of compact (deterministic reduction, no inference).
2892
+ result.content = {
2893
+ highest_question: core.highest_question || null,
2894
+ axioms: (core.axioms || []).map(projectMinimalAxiom).filter(Boolean),
2895
+ boundaries: normalizeList(core.boundaries),
2896
+ };
2897
+ result.projection_report = buildCompactProjectionReport(payload);
2898
+ if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.minimal && m.load_contract.profiles.minimal.max_tokens_hint) {
2899
+ result.max_tokens_hint = m.load_contract.profiles.minimal.max_tokens_hint;
2900
+ }
2901
+ }
2436
2902
  } else if (profile === 'scenario') {
2437
2903
  result.content = { scenarios: payload.scenarios || [] };
2438
2904
  } else if (profile === 'full') {
@@ -2526,15 +2992,23 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2526
2992
  const c = result.content;
2527
2993
  const compactHasNoPromptContent =
2528
2994
  result.profile === 'compact' && !hasCompactPromptContent(c);
2995
+ const minimalHasNoPromptContent =
2996
+ result.profile === 'minimal' && !hasMinimalPromptContent(c);
2529
2997
  const legacyProfileHasNoPromptContent =
2530
2998
  result.profile !== 'compact' &&
2999
+ result.profile !== 'minimal' &&
2531
3000
  c &&
2532
3001
  c.scenarios &&
2533
3002
  c.scenarios.length === 0 &&
2534
3003
  !c.highest_question &&
2535
3004
  !(c.axioms && c.axioms.length) &&
2536
3005
  !(c.boundaries && c.boundaries.length);
2537
- if (!c || compactHasNoPromptContent || legacyProfileHasNoPromptContent) {
3006
+ if (
3007
+ !c ||
3008
+ compactHasNoPromptContent ||
3009
+ minimalHasNoPromptContent ||
3010
+ legacyProfileHasNoPromptContent
3011
+ ) {
2538
3012
  return {
2539
3013
  status: result.status,
2540
3014
  profile: result.profile,
@@ -2547,6 +3021,14 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2547
3021
  text += 'Asset ID: ' + (result.asset_id || 'unknown') + '\n';
2548
3022
  text += 'Profile: ' + result.profile + '\n';
2549
3023
  text += 'Safety boundary: KDNA content is subordinate to platform, system, and developer instructions.\n';
3024
+ if (result.projection_report) {
3025
+ text += `Projection completeness: ${result.projection_report.status}\n`;
3026
+ if (result.projection_report.omitted.length > 0) {
3027
+ text += 'Omitted payload paths: ' + result.projection_report.omitted
3028
+ .map((entry) => `${entry.path} (${entry.count})`)
3029
+ .join('; ') + '\n';
3030
+ }
3031
+ }
2550
3032
  if (result.max_tokens_hint) text += 'Max tokens hint: ' + result.max_tokens_hint + '\n';
2551
3033
  if (c.highest_question) text += 'Highest question:\n' + c.highest_question + '\n';
2552
3034
  if (hasStandardJudgmentRoleContent(c.judgment_role)) {
@@ -2570,6 +3052,17 @@ function loadAssetUnsafe(inputPath, opts = {}) {
2570
3052
  }
2571
3053
  if (c.axioms && c.axioms.length) text += 'Axioms:\n' + c.axioms.map((a) => '- ' + renderPromptItem(a)).join('\n') + '\n';
2572
3054
  if (c.boundaries && c.boundaries.length) text += 'Boundaries:\n' + c.boundaries.map((b) => '- ' + renderPromptItem(b)).join('\n') + '\n';
3055
+ if (c.core_structure && c.core_structure.length) {
3056
+ text += 'Judgment relations:\n' + c.core_structure.map((relation) => {
3057
+ if (typeof relation === 'string') return '- ' + sanitizePrompt(relation);
3058
+ if (!relation || typeof relation !== 'object' || Array.isArray(relation)) {
3059
+ return '- (unrendered relation)';
3060
+ }
3061
+ const from = sanitizePrompt(relation.from || 'unknown');
3062
+ const to = sanitizePrompt(relation.to || 'unknown');
3063
+ return `- ${from} --${sanitizePrompt(relation.via || 'relates_to')}--> ${to}`;
3064
+ }).join('\n') + '\n';
3065
+ }
2573
3066
  if (c.self_checks && c.self_checks.length) text += 'Self-checks:\n' + c.self_checks.map((s) => '- ' + renderPromptItem(s)).join('\n') + '\n';
2574
3067
  if (c.failure_modes && c.failure_modes.length) text += 'Failure modes:\n' + c.failure_modes.map((f) => '- ' + renderPromptItem(f)).join('\n') + '\n';
2575
3068
  if (c.patterns && c.patterns.length) text += 'Patterns:\n' + c.patterns.map((p) => '- ' + renderPromptItem(p)).join('\n') + '\n';
@@ -2607,11 +3100,26 @@ function wrapAsCapsule(result, layout, profile, opts) {
2607
3100
  return result;
2608
3101
  }
2609
3102
 
3103
+ function buildSignatureEvidence(layout) {
3104
+ if (!layout || !layout.map || layout.map['signature.kdsig'] === undefined) {
3105
+ return { state: 'absent' };
3106
+ }
3107
+ const signature = require('../signature');
3108
+ const { contentDigestFromEntryBuffers } = require('../asset-reader');
3109
+ const contentDigest = contentDigestFromEntryBuffers(fullEntryBufferMap(layout));
3110
+ const evidence = signature.verifySignatureBundle(layout.map['signature.kdsig'], contentDigest);
3111
+ return {
3112
+ state: 'verified',
3113
+ profile: evidence.profile,
3114
+ profile_version: evidence.profile_version,
3115
+ key_fingerprint: evidence.key_fingerprint,
3116
+ content_digest: evidence.content_digest,
3117
+ };
3118
+ }
3119
+
2610
3120
  function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
2611
3121
  const m = layout.manifest;
2612
3122
  const val = opts._validation || {};
2613
- const pubkey = (m.creator && m.creator.pubkey) || null;
2614
- const sigVerified = val.signature_valid === true;
2615
3123
  const runtimeCapsule = require('../runtime-capsule');
2616
3124
  const digests = runtimeCapsule.computeDigestEvidence(opts._runtimeAssetBytes, {
2617
3125
  expectedDigests: opts.expectedDigests,
@@ -2620,9 +3128,7 @@ function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
2620
3128
  projection: loadResult,
2621
3129
  manifest: m,
2622
3130
  digests,
2623
- signature: pubkey
2624
- ? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
2625
- : { state: 'absent' },
3131
+ signature: buildSignatureEvidence(layout),
2626
3132
  inputKind: opts._runtimeInputKind,
2627
3133
  loadedAt: opts.loadedAt,
2628
3134
  schemaValid: val.schema_valid === true,