@aikdna/kdna-core 0.17.0 → 0.19.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +227 -64
  2. package/README.md +114 -77
  3. package/package.json +8 -2
  4. package/schema/agent-host-capabilities.schema.json +62 -0
  5. package/schema/agent-host-receipt.schema.json +371 -0
  6. package/schema/agent-host-request.schema.json +223 -0
  7. package/schema/{bundle-profile-v1.schema.json → bundle-profile.schema.json} +9 -5
  8. package/schema/checksums.schema.json +42 -3
  9. package/schema/consumption-plan.schema.json +428 -0
  10. package/schema/digest-evidence.schema.json +212 -0
  11. package/schema/{kdna-envelope-external-grant-v1.schema.json → external-grant-envelope.schema.json} +17 -4
  12. package/schema/{kdna-key-grant-v1.schema.json → external-key-grant.schema.json} +39 -7
  13. package/schema/judgment-trace.schema.json +1251 -0
  14. package/schema/load-plan.schema.json +7 -5
  15. package/schema/manifest.schema.json +70 -10
  16. package/schema/{payload-profile-v1.schema.json → payload-profile.schema.json} +30 -6
  17. package/schema/runtime-capsule.schema.json +116 -0
  18. package/src/asset-reader.js +106 -120
  19. package/src/{v1 → container}/index.js +566 -209
  20. package/src/container/index.mjs +20 -0
  21. package/src/container-dispatcher.js +10 -7
  22. package/src/crypto-profile.js +37 -107
  23. package/src/external-key-grant.js +41 -15
  24. package/src/index.js +11 -4
  25. package/src/index.mjs +143 -24
  26. package/src/loader-compatibility.js +59 -0
  27. package/src/loader.js +1 -1
  28. package/src/public-api.js +59 -163
  29. package/src/remote-runtime.d.ts +13 -0
  30. package/src/remote-runtime.js +27 -0
  31. package/src/remote-runtime.mjs +3 -0
  32. package/src/runtime-api.js +9 -10
  33. package/src/runtime-capsule.js +529 -0
  34. package/src/runtime-contract.js +1882 -0
  35. package/src/types.d.ts +1127 -136
  36. package/src/workpack-pure.js +1 -1
  37. package/src/lint-pure.js +0 -430
  38. package/src/v1/index.mjs +0 -20
@@ -6,15 +6,14 @@ const fs = require('fs');
6
6
  const crypto = require('crypto');
7
7
  const zlib = require('zlib');
8
8
  const cbor = require('cbor-x');
9
- const { loadDomainFromFiles, formatContext } = require('./loader');
9
+ const { loaderVersionUnsupportedMessage } = require('./loader-compatibility');
10
10
 
11
11
  const KDNA_MEDIA_TYPE = 'application/vnd.kdna.asset';
12
+ const ASSET_SOURCE_BYTES = Symbol.for('@aikdna/kdna-core.asset-source-bytes');
12
13
 
13
- // Standard KDNA domain data entries — kept in lockstep with loader.FILE_MAP.
14
- // Asset reader pre-loads these so callers don't have to enumerate entry names.
15
- // NOTE: kdna.json / manifest.json are read separately via readManifest(); do not
16
- // add them here. KDNA_Cluster is intentionally omitted — its schema exists but
17
- // is not yet wired into the loader contract.
14
+ // Deprecated legacy authoring source entry names. Current Runtime assets carry
15
+ // judgment only in payload.kdnab and never expose these as distribution entries.
16
+ // Kept for source-migration callers; loadProfile does not use this list.
18
17
  const STANDARD_ENTRIES = Object.freeze([
19
18
  'KDNA_Core.json',
20
19
  'KDNA_Patterns.json',
@@ -43,6 +42,14 @@ function stableStringify(value) {
43
42
  return JSON.stringify(value);
44
43
  }
45
44
 
45
+ function compareEntryNamesByUtf8(left, right) {
46
+ return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
47
+ }
48
+
49
+ function sortedEntryNames(entries) {
50
+ return [...entries.keys()].sort(compareEntryNamesByUtf8);
51
+ }
52
+
46
53
  function parseJson(buf, entryName) {
47
54
  try {
48
55
  return JSON.parse(Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf));
@@ -51,6 +58,17 @@ function parseJson(buf, entryName) {
51
58
  }
52
59
  }
53
60
 
61
+ function validateDecodedEntry(buf, entryName) {
62
+ if (entryName === 'payload.kdnab') {
63
+ try {
64
+ return cbor.decode(buf);
65
+ } catch (e) {
66
+ throw new Error(`${entryName}: invalid CBOR: ${e.message}`, { cause: e });
67
+ }
68
+ }
69
+ return parseJson(buf, entryName);
70
+ }
71
+
54
72
  function encryptedEntries(manifest) {
55
73
  const entries = manifest?.encryption?.encrypted_entries;
56
74
  return Array.isArray(entries) ? entries : [];
@@ -188,7 +206,7 @@ function manifestForDigest(manifest) {
188
206
  function buildContentDigest(asset) {
189
207
  const parts = [];
190
208
  const excluded = new Set(['.DS_Store', 'signature.json', 'build-receipt.json']);
191
- for (const entryName of [...asset.entries.keys()].sort()) {
209
+ for (const entryName of sortedEntryNames(asset.entries)) {
192
210
  if (excluded.has(entryName)) continue;
193
211
  if (entryName.startsWith('reports/')) continue;
194
212
  const entryBuf = asset.readEntry(entryName);
@@ -243,7 +261,7 @@ function buildSigningPayload(asset, options = {}) {
243
261
  // byte strings and the signature would fail to verify. The fix
244
262
  // mirrors `buildContentDigest`'s exclusion set exactly.
245
263
  const parts = [];
246
- for (const entryName of [...asset.entries.keys()].sort()) {
264
+ for (const entryName of sortedEntryNames(asset.entries)) {
247
265
  if (entryName === '.DS_Store' || entryName === 'signature.json') continue;
248
266
  if (entryName === 'build-receipt.json') continue;
249
267
  if (entryName.startsWith('reports/')) continue;
@@ -324,7 +342,9 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
324
342
  ['stances', 'stance'],
325
343
  ];
326
344
 
327
- let verified = 0, missing = 0, invalid = 0;
345
+ let verified = 0,
346
+ missing = 0,
347
+ invalid = 0;
328
348
  for (const [arrayName] of CORE_CARD_ARRAYS) {
329
349
  const cards = coreData?.[arrayName];
330
350
  if (!Array.isArray(cards) || cards.length === 0) continue;
@@ -342,8 +362,11 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
342
362
  const sig = Buffer.from(String(hl.signature).replace(/^ed25519:/, ''), 'hex');
343
363
  const key = crypto.createPublicKey(publicKeyPEM);
344
364
  const ok = crypto.verify(null, Buffer.from(payload), key, sig);
345
- if (ok) { verified++; }
346
- else { invalid++; }
365
+ if (ok) {
366
+ verified++;
367
+ } else {
368
+ invalid++;
369
+ }
347
370
  } catch {
348
371
  invalid++;
349
372
  }
@@ -351,7 +374,9 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
351
374
  }
352
375
 
353
376
  if (invalid > 0) {
354
- errors.push(`${invalid} Human Lock signature(s) failed verification — judgment may have been altered`);
377
+ errors.push(
378
+ `${invalid} Human Lock signature(s) failed verification — judgment may have been altered`,
379
+ );
355
380
  }
356
381
  if (missing > 0) {
357
382
  warnings.push(`${missing} locked card(s) have no Human Lock signature`);
@@ -361,29 +386,10 @@ function verifyHumanLockSignatures(coreData, manifest, errors, warnings) {
361
386
  }
362
387
  }
363
388
 
364
- function validateManifestIdentity(manifest, errors, _warnings) {
365
- if (manifest.kdna_spec) {
366
- errors.push('kdna.json: kdna_spec is not allowed. Use kdna_version.');
367
- }
368
- if (manifest.format && manifest.format !== 'kdna') {
369
- errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
370
- }
371
- if (!manifest.kdna_version) {
372
- errors.push('kdna.json: missing required field "kdna_version"');
373
- } else if (manifest.kdna_version !== '1.0') {
374
- errors.push(
375
- `kdna.json.kdna_version: invalid value "${manifest.kdna_version}". Expected "1.0".`,
376
- );
377
- }
378
- if (manifest.language) {
379
- errors.push('kdna.json: language is not allowed. Use default_language and languages.');
380
- }
381
- }
382
-
383
389
  function openAsset(input) {
384
390
  const { buffer, path } = normalizeInput(input);
385
391
  const entries = parseZipEntries(buffer);
386
- return {
392
+ const asset = {
387
393
  path,
388
394
  size: buffer.length,
389
395
  asset_digest: `sha256:${sha256Hex(buffer)}`,
@@ -394,6 +400,34 @@ function openAsset(input) {
394
400
  return readZipEntry(buffer, entry);
395
401
  },
396
402
  };
403
+ Object.defineProperty(asset, ASSET_SOURCE_BYTES, {
404
+ value: buffer,
405
+ enumerable: false,
406
+ writable: false,
407
+ });
408
+ return asset;
409
+ }
410
+
411
+ function validateCurrentContainer(asset) {
412
+ const bytes = asset[ASSET_SOURCE_BYTES];
413
+ if (!Buffer.isBuffer(bytes)) {
414
+ throw new Error('Current KDNA validation requires the original .kdna container bytes');
415
+ }
416
+ return require('./container').validate(bytes);
417
+ }
418
+
419
+ function appendCurrentValidationErrors(asset, errors) {
420
+ try {
421
+ const validation = validateCurrentContainer(asset);
422
+ if (!validation.overall_valid) errors.push(...validation.problems);
423
+ if (validation.loader_compatible === false) {
424
+ errors.push(
425
+ loaderVersionUnsupportedMessage(validation.min_loader_version),
426
+ );
427
+ }
428
+ } catch (e) {
429
+ errors.push(e.message);
430
+ }
397
431
  }
398
432
 
399
433
  function listEntries(asset) {
@@ -409,30 +443,18 @@ function readManifest(asset) {
409
443
  return parseJson(asset.readEntry('kdna.json'), 'kdna.json');
410
444
  }
411
445
 
412
- function readDataMapSync(asset, options = {}) {
413
- const dataMap = {};
414
- const manifest = readManifest(asset);
415
-
446
+ function readDataMapSync(asset, _options = {}) {
416
447
  if (asset.entries.has('payload.kdnab')) {
417
- const payloadBuf = asset.readEntry('payload.kdnab');
418
- const payload = cbor.decode(payloadBuf);
419
- if (payload && payload.judgment) {
420
- if (payload.judgment.core) dataMap['KDNA_Core.json'] = payload.judgment.core;
421
- if (payload.judgment.patterns) dataMap['KDNA_Patterns.json'] = payload.judgment.patterns;
422
- if (payload.judgment.scenarios) dataMap['KDNA_Scenarios.json'] = payload.judgment.scenarios;
423
- if (payload.judgment.cases) dataMap['KDNA_Cases.json'] = payload.judgment.cases;
424
- if (payload.judgment.reasoning) dataMap['KDNA_Reasoning.json'] = payload.judgment.reasoning;
425
- if (payload.judgment.evolution) dataMap['KDNA_Evolution.json'] = payload.judgment.evolution;
426
- }
427
-
428
- const encrypted = encryptedEntries(manifest);
429
- if (encrypted.length && typeof options.decryptEntry !== 'function') {
430
- throw new Error(`encrypted entries require decryptEntry hook: ${encrypted.join(', ')}`);
431
- }
432
- return dataMap;
448
+ const error = new Error(
449
+ 'readDataMap is a legacy source-tree API and does not project current payload.kdnab into KDNA_Core.json/KDNA_Patterns.json. Use loadProfile or loadAuthorized.',
450
+ );
451
+ error.code = 'KDNA_LEGACY_DATA_MAP_UNSUPPORTED';
452
+ throw error;
433
453
  }
434
454
 
435
- throw new Error('Not a KDNA asset: missing payload.kdnab');
455
+ const error = new Error('Not a current KDNA asset: missing payload.kdnab');
456
+ error.code = 'KDNA_FORMAT_INVALID';
457
+ throw error;
436
458
  }
437
459
 
438
460
  function verifySync(asset, options = {}) {
@@ -440,6 +462,12 @@ function verifySync(asset, options = {}) {
440
462
  const warnings = [];
441
463
  const entries = listEntries(asset);
442
464
 
465
+ // The current implementation is the sole authority for container,
466
+ // manifest, CBOR payload, checksum, and load-contract validity. The reader
467
+ // adds transport identity and legacy signature hooks; it must not maintain a
468
+ // second manifest or payload dialect.
469
+ appendCurrentValidationErrors(asset, errors);
470
+
443
471
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
444
472
  verifyMediaType(asset, errors);
445
473
  if (!asset.entries.has('payload.kdnab')) errors.push('required entry missing: payload.kdnab');
@@ -450,7 +478,9 @@ function verifySync(asset, options = {}) {
450
478
  errors.push(`asset digest mismatch: expected ${options.asset_digest}, got ${asset_digest}`);
451
479
  }
452
480
  if (options.content_digest && options.content_digest !== content_digest) {
453
- errors.push(`content digest mismatch: expected ${options.content_digest}, got ${content_digest}`);
481
+ errors.push(
482
+ `content digest mismatch: expected ${options.content_digest}, got ${content_digest}`,
483
+ );
454
484
  }
455
485
 
456
486
  let manifest = null;
@@ -458,7 +488,6 @@ function verifySync(asset, options = {}) {
458
488
  if (asset.entries.has('kdna.json')) {
459
489
  try {
460
490
  manifest = readManifest(asset);
461
- validateManifestIdentity(manifest, errors, warnings);
462
491
  const encrypted = encryptedEntries(manifest);
463
492
  if (encrypted.length) {
464
493
  warnings.push(`encrypted entries present: ${encrypted.join(', ')}`);
@@ -479,7 +508,7 @@ function verifySync(asset, options = {}) {
479
508
  asset.readEntry(entryName),
480
509
  options,
481
510
  );
482
- parseJson(decrypted, entryName);
511
+ validateDecodedEntry(decrypted, entryName);
483
512
  } catch (e) {
484
513
  errors.push(e.message);
485
514
  }
@@ -516,29 +545,15 @@ function verifySync(asset, options = {}) {
516
545
  }
517
546
 
518
547
  function loadProfileSync(asset, profile = 'compact', options = {}) {
519
- const manifest = readManifest(asset);
520
- if (profile === 'index') {
521
- return {
522
- profile,
523
- manifest,
524
- asset_digest: asset.asset_digest,
525
- content_digest: buildContentDigest(asset),
526
- entries: listEntries(asset),
527
- name: manifest.name || manifest.asset_id || null,
528
- version: manifest.version || null,
529
- judgment_version: manifest.judgment_version || null,
530
- keywords: manifest.keywords || [],
531
- };
532
- }
533
- const dataMap = readDataMapSync(asset, STANDARD_ENTRIES, options);
534
- const mode = profile === 'full' ? 'all' : profile === 'scenario' ? 'auto' : 'minimum';
535
- const domain = loadDomainFromFiles(dataMap, { mode, input: options.input || '' });
536
- return {
548
+ const bytes = asset[ASSET_SOURCE_BYTES];
549
+ if (!Buffer.isBuffer(bytes)) {
550
+ throw new Error('loadProfile requires the original .kdna container bytes');
551
+ }
552
+ return require('./runtime-api').loadAuthorized(bytes, {
553
+ ...options,
537
554
  profile,
538
- manifest,
539
- domain,
540
- context: options.context === false || !domain ? null : formatContext(domain),
541
- };
555
+ as: options.as || 'json',
556
+ });
542
557
  }
543
558
 
544
559
  function createKdnaAssetReader() {
@@ -565,7 +580,13 @@ function createKdnaAssetReader() {
565
580
  if (!asset.entries.has(entryName)) return null;
566
581
  const manifest =
567
582
  entryName === 'kdna.json' ? null : parseJson(asset.readEntry('kdna.json'), 'kdna.json');
568
- const buf = maybeDecryptEntrySync(asset, manifest, entryName, asset.readEntry(entryName), options);
583
+ const buf = maybeDecryptEntrySync(
584
+ asset,
585
+ manifest,
586
+ entryName,
587
+ asset.readEntry(entryName),
588
+ options,
589
+ );
569
590
  return parseJson(buf, entryName);
570
591
  },
571
592
 
@@ -591,21 +612,8 @@ function createKdnaAssetReader() {
591
612
 
592
613
  readDataMapSync,
593
614
 
594
- async readDataMap(asset, entries = STANDARD_ENTRIES, options = {}) {
595
- const dataMap = {};
596
- const manifest = await this.readManifest(asset);
597
- for (const entryName of entries) {
598
- if (!asset.entries.has(entryName)) continue;
599
- const buf = await maybeDecryptEntry(
600
- asset,
601
- manifest,
602
- entryName,
603
- asset.readEntry(entryName),
604
- options,
605
- );
606
- dataMap[entryName] = parseJson(buf, entryName);
607
- }
608
- return dataMap;
615
+ async readDataMap(asset, _entries = STANDARD_ENTRIES, options = {}) {
616
+ return readDataMapSync(asset, options);
609
617
  },
610
618
 
611
619
  contentDigestSync: buildContentDigest,
@@ -621,6 +629,8 @@ function createKdnaAssetReader() {
621
629
  const warnings = [];
622
630
  const entries = [...asset.entries.keys()].sort();
623
631
 
632
+ appendCurrentValidationErrors(asset, errors);
633
+
624
634
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
625
635
  verifyMediaType(asset, errors);
626
636
  if (!asset.entries.has('payload.kdnab')) errors.push('required entry missing: payload.kdnab');
@@ -641,7 +651,6 @@ function createKdnaAssetReader() {
641
651
  if (asset.entries.has('kdna.json')) {
642
652
  try {
643
653
  manifest = parseJson(asset.readEntry('kdna.json'), 'kdna.json');
644
- validateManifestIdentity(manifest, errors, warnings);
645
654
  const encrypted = encryptedEntries(manifest);
646
655
  if (encrypted.length) {
647
656
  warnings.push(`encrypted entries present: ${encrypted.join(', ')}`);
@@ -662,7 +671,7 @@ function createKdnaAssetReader() {
662
671
  asset.readEntry(entryName),
663
672
  options,
664
673
  );
665
- parseJson(decrypted, entryName);
674
+ validateDecodedEntry(decrypted, entryName);
666
675
  } catch (e) {
667
676
  errors.push(e.message);
668
677
  }
@@ -692,30 +701,7 @@ function createKdnaAssetReader() {
692
701
  loadProfileSync,
693
702
 
694
703
  async loadProfile(asset, profile = 'compact', options = {}) {
695
- const manifest = await this.readManifest(asset);
696
- if (profile === 'index') {
697
- return {
698
- profile,
699
- manifest,
700
- asset_digest: asset.asset_digest,
701
- content_digest: buildContentDigest(asset),
702
- entries: await this.listEntries(asset),
703
- name: manifest.name || manifest.asset_id || null,
704
- version: manifest.version || null,
705
- judgment_version: manifest.judgment_version || null,
706
- keywords: manifest.keywords || [],
707
- };
708
- }
709
-
710
- const dataMap = await this.readDataMap(asset, STANDARD_ENTRIES, options);
711
- const mode = profile === 'full' ? 'all' : profile === 'scenario' ? 'auto' : 'minimum';
712
- const domain = loadDomainFromFiles(dataMap, { mode, input: options.input || '' });
713
- return {
714
- profile,
715
- manifest,
716
- domain,
717
- context: options.context === false || !domain ? null : formatContext(domain),
718
- };
704
+ return loadProfileSync(asset, profile, options);
719
705
  },
720
706
  };
721
707
  }