@aikdna/kdna-core 0.18.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 (44) hide show
  1. package/CHANGELOG.md +128 -94
  2. package/README.md +109 -215
  3. package/package.json +7 -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 +8 -8
  9. package/schema/consumption-plan.schema.json +428 -0
  10. package/schema/digest-evidence.schema.json +115 -26
  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 +5 -3
  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-2.schema.json → runtime-capsule.schema.json} +7 -31
  18. package/src/asset-reader.js +8 -2
  19. package/src/{v1 → container}/index.js +385 -198
  20. package/src/container/index.mjs +20 -0
  21. package/src/container-dispatcher.js +6 -6
  22. package/src/crypto-profile.js +37 -107
  23. package/src/external-key-grant.js +41 -15
  24. package/src/index.js +11 -8
  25. package/src/index.mjs +49 -45
  26. package/src/loader-compatibility.js +59 -0
  27. package/src/loader.js +1 -1
  28. package/src/public-api.js +5 -5
  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 +5 -5
  33. package/src/{capsule-v2.js → runtime-capsule.js} +96 -254
  34. package/src/{execution-contract-v1.js → runtime-contract.js} +184 -152
  35. package/src/types.d.ts +200 -277
  36. package/src/workpack-pure.js +1 -1
  37. package/schema/agent-host-2-receipt.schema.json +0 -206
  38. package/schema/agent-host-2-request.schema.json +0 -135
  39. package/schema/agent-host-capabilities-1.schema.json +0 -40
  40. package/schema/consumption-plan-1.schema.json +0 -280
  41. package/schema/judgment-trace-1.schema.json +0 -748
  42. package/schema/runtime-capsule-1.schema.json +0 -68
  43. package/src/lint-pure.js +0 -433
  44. package/src/v1/index.mjs +0 -21
@@ -20,7 +20,10 @@
20
20
  * - the source directory must contain mimetype, kdna.json, payload.kdnab
21
21
  * - checksums.json and signatures/ are optional
22
22
  * - lineage must be a single object (not an array)
23
- * - pack output must be deterministic: same input → same SHA-256
23
+ * - pack output is reproducible within one pinned toolchain/compressor;
24
+ * DEFLATE bytes may differ across compressors, zlib versions, or systems
25
+ * - entry_set_digest/source_fingerprint identify logical entries; A binds
26
+ * the exact immutable package bytes
24
27
  *
25
28
  * Output language must stay content-neutral. We never say "trusted",
26
29
  * "recommended", "high_quality", or "officially_approved". We say
@@ -37,6 +40,11 @@ const path = require('node:path');
37
40
  const zlib = require('node:zlib');
38
41
  const crypto = require('node:crypto');
39
42
  const cbor = require('cbor-x');
43
+ const {
44
+ KDNA_LOADER_VERSION,
45
+ assessLoaderCompatibility,
46
+ loaderVersionUnsupportedMessage,
47
+ } = require('../loader-compatibility');
40
48
  const {
41
49
  EXTERNAL_ENVELOPE_PROFILE,
42
50
  validateExternalEnvelope,
@@ -67,12 +75,6 @@ const FORBIDDEN_LEGACY_TOP_LEVEL = new Set([
67
75
  'KDNA_Evolution.json',
68
76
  ]);
69
77
 
70
- // Legacy aliases — prefer the unversioned names above
71
- const V1_REQUIRED_DIR_ENTRIES = REQUIRED_DIR_ENTRIES;
72
- const V1_OPTIONAL_DIR_ENTRIES = OPTIONAL_DIR_ENTRIES;
73
- const V1_ALLOWED_TOP_LEVEL_ENTRIES = ALLOWED_TOP_LEVEL_ENTRIES;
74
- const V1_FORBIDDEN_LEGACY_TOP_LEVEL = FORBIDDEN_LEGACY_TOP_LEVEL;
75
-
76
78
  const DEFAULT_CONTAINER_LIMITS = Object.freeze({
77
79
  maxContainerBytes: 25 * 1024 * 1024,
78
80
  maxEntries: 128,
@@ -84,7 +86,7 @@ const DEFAULT_CONTAINER_LIMITS = Object.freeze({
84
86
  maxJsonStringLength: 1024 * 1024,
85
87
  });
86
88
 
87
- // Words that must never appear in v1 CLI output as positive claims.
89
+ // Words that must never appear in layout CLI output as positive claims.
88
90
  // Schema-valid, signature-valid, compatible — those are fine.
89
91
  // "trusted", "recommended", "high_quality", "officially_approved" — never.
90
92
  const FORBIDDEN_OUTPUT_TERMS = Object.freeze([
@@ -103,8 +105,8 @@ let _validators = null;
103
105
  function loadPackagedSchemas() {
104
106
  return {
105
107
  manifestSchema: require('../../schema/manifest.schema.json'),
106
- payloadSchema: require('../../schema/payload-profile-v1.schema.json'),
107
- bundlePayloadSchema: require('../../schema/bundle-profile-v1.schema.json'),
108
+ payloadSchema: require('../../schema/payload-profile.schema.json'),
109
+ bundlePayloadSchema: require('../../schema/bundle-profile.schema.json'),
108
110
  checksumsSchema: require('../../schema/checksums.schema.json'),
109
111
  loadContractSchema: require('../../schema/load-contract.schema.json'),
110
112
  };
@@ -131,10 +133,10 @@ function loadSchemasFromDisk() {
131
133
  return {
132
134
  manifestSchema: JSON.parse(fs.readFileSync(path.join(schemaDir, 'manifest.schema.json'), 'utf8')),
133
135
  payloadSchema: JSON.parse(
134
- fs.readFileSync(path.join(schemaDir, 'payload-profile-v1.schema.json'), 'utf8'),
136
+ fs.readFileSync(path.join(schemaDir, 'payload-profile.schema.json'), 'utf8'),
135
137
  ),
136
138
  bundlePayloadSchema: JSON.parse(
137
- fs.readFileSync(path.join(schemaDir, 'bundle-profile-v1.schema.json'), 'utf8'),
139
+ fs.readFileSync(path.join(schemaDir, 'bundle-profile.schema.json'), 'utf8'),
138
140
  ),
139
141
  checksumsSchema: JSON.parse(
140
142
  fs.readFileSync(path.join(schemaDir, 'checksums.schema.json'), 'utf8'),
@@ -187,7 +189,7 @@ function loadSchemas() {
187
189
  */
188
190
  function isKdnaSourceDir(absPath) {
189
191
  if (!fs.existsSync(absPath) || !fs.statSync(absPath).isDirectory()) return false;
190
- for (const f of V1_REQUIRED_DIR_ENTRIES) {
192
+ for (const f of REQUIRED_DIR_ENTRIES) {
191
193
  if (!fs.existsSync(path.join(absPath, f))) return false;
192
194
  }
193
195
  const mime = fs.readFileSync(path.join(absPath, 'mimetype'), 'utf8');
@@ -397,8 +399,8 @@ function validateContainerEntryMetadata(entry) {
397
399
  throw new Error(`container has duplicate entry: ${normalizedName}`);
398
400
  }
399
401
  const topLevel = normalizedName.split('/')[0];
400
- if (!V1_ALLOWED_TOP_LEVEL_ENTRIES.has(topLevel)) {
401
- if (V1_FORBIDDEN_LEGACY_TOP_LEVEL.has(topLevel)) {
402
+ if (!ALLOWED_TOP_LEVEL_ENTRIES.has(topLevel)) {
403
+ if (FORBIDDEN_LEGACY_TOP_LEVEL.has(topLevel)) {
402
404
  throw new Error(`container includes forbidden top-level source entry: ${topLevel}`);
403
405
  }
404
406
  throw new Error(`container includes unsupported top-level entry: ${topLevel}`);
@@ -516,17 +518,17 @@ function listSourceDir(dir, opts = {}) {
516
518
  return out;
517
519
  }
518
520
 
519
- // ─── Read v1 from either source dir or container ───────────────────────
521
+ // ─── Read layout from either source dir or container ───────────────────────
520
522
 
521
523
  /**
522
- * Read a v1 layout (source dir or .kdna container) and return a single
524
+ * Read a layout layout (source dir or .kdna container) and return a single
523
525
  * normalized map of { mimetype, kdna.json, payload.kdnab, checksums.json? }.
524
526
  * `where` describes the origin for error messages.
525
527
  *
526
528
  * Throws an Error with a clear, content-neutral message if the layout
527
529
  * is malformed (missing entry, wrong mimetype, etc.).
528
530
  */
529
- function readV1Layout(absPath) {
531
+ function readLayout(absPath) {
530
532
  let stat;
531
533
  try {
532
534
  stat = fs.statSync(absPath);
@@ -541,7 +543,7 @@ function readV1Layout(absPath) {
541
543
 
542
544
  if (stat.isDirectory()) {
543
545
  kind = 'dir';
544
- for (const f of V1_REQUIRED_DIR_ENTRIES) {
546
+ for (const f of REQUIRED_DIR_ENTRIES) {
545
547
  const full = path.join(absPath, f);
546
548
  if (!fs.existsSync(full)) {
547
549
  throw new Error(`not a KDNA authoring source directory: missing ${f}`);
@@ -550,7 +552,7 @@ function readV1Layout(absPath) {
550
552
  throw new Error(`not a KDNA authoring source directory: ${f} must not be a symlink`);
551
553
  }
552
554
  }
553
- for (const f of [...V1_REQUIRED_DIR_ENTRIES, ...V1_OPTIONAL_DIR_ENTRIES]) {
555
+ for (const f of [...REQUIRED_DIR_ENTRIES, ...OPTIONAL_DIR_ENTRIES]) {
554
556
  const full = path.join(absPath, f);
555
557
  if (fs.existsSync(full)) {
556
558
  if (fs.statSync(full).isFile()) {
@@ -587,17 +589,17 @@ function readV1Layout(absPath) {
587
589
  ? `sha256:${crypto.createHash('sha256').update(containerBytes).digest('hex')}`
588
590
  : null;
589
591
 
590
- return finalizeV1Layout({ kind, map, entries, containerDigest });
592
+ return finalizeContainerLayout({ kind, map, entries, containerDigest });
591
593
  }
592
594
 
593
- function finalizeV1Layout({ kind, map, entries, containerDigest }) {
595
+ function finalizeContainerLayout({ kind, map, entries, containerDigest }) {
594
596
  if (kind !== 'dir' && (!entries || entries.length === 0 || entries[0].name !== 'mimetype')) {
595
597
  throw new Error('not a KDNA Asset Container: first entry is not mimetype');
596
598
  }
597
599
  if (kind !== 'dir' && entries[0].method !== 0) {
598
600
  throw new Error('not a KDNA Asset Container: mimetype must be uncompressed');
599
601
  }
600
- for (const required of V1_REQUIRED_DIR_ENTRIES) {
602
+ for (const required of REQUIRED_DIR_ENTRIES) {
601
603
  if (!map[required]) {
602
604
  const source = kind === 'dir' ? 'KDNA authoring source directory' : 'KDNA Asset Container';
603
605
  throw new Error(`not a ${source}: missing ${required}`);
@@ -622,7 +624,7 @@ function finalizeV1Layout({ kind, map, entries, containerDigest }) {
622
624
  return { kind, map, manifest, entries, containerDigest };
623
625
  }
624
626
 
625
- function readV1LayoutBytes(input) {
627
+ function readLayoutBytes(input) {
626
628
  const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
627
629
  const entries = listZipEntriesFromBuffer(bytes);
628
630
  const map = {};
@@ -636,7 +638,7 @@ function readV1LayoutBytes(input) {
636
638
  map[entry.name] = entry.data;
637
639
  }
638
640
  }
639
- return finalizeV1Layout({
641
+ return finalizeContainerLayout({
640
642
  kind: 'memory',
641
643
  map,
642
644
  entries,
@@ -646,9 +648,9 @@ function readV1LayoutBytes(input) {
646
648
 
647
649
  function readInputLayout(input) {
648
650
  if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
649
- return readV1LayoutBytes(input);
651
+ return readLayoutBytes(input);
650
652
  }
651
- return readV1Layout(path.resolve(input));
653
+ return readLayout(path.resolve(input));
652
654
  }
653
655
 
654
656
  function snapshotPackagedInput(input) {
@@ -724,7 +726,7 @@ function parsePayloadEntry(name, bytes, opts = {}) {
724
726
  try {
725
727
  parsed = cbor.decode(bytes);
726
728
  } catch {
727
- throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (kdna_version 1.0).`);
729
+ throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (format 0.1.0).`);
728
730
  }
729
731
  assertJsonWithinLimits(parsed, name, limits);
730
732
  return parsed;
@@ -736,10 +738,10 @@ function parsePayloadEntry(name, bytes, opts = {}) {
736
738
  * Print a content-neutral manifest summary. Always JSON. Never emits
737
739
  * the words trusted / recommended / high_quality / officially_approved.
738
740
  */
739
- function buildInspectOutput(v1) {
740
- const m = v1.manifest;
741
+ function buildInspectOutput(container) {
742
+ const m = container.manifest;
741
743
  const out = {
742
- kdna_version: m.kdna_version ?? null,
744
+ format_version: m.format_version ?? null,
743
745
  asset_id: m.asset_id ?? null,
744
746
  asset_uid: m.asset_uid ?? null,
745
747
  asset_type: m.asset_type ?? null,
@@ -749,10 +751,11 @@ function buildInspectOutput(v1) {
749
751
  payload: m.payload ? m.payload.path : null,
750
752
  payload_encrypted: m.payload ? m.payload.encrypted : null,
751
753
  profile: m.compatibility ? m.compatibility.profile : null,
754
+ ...assessLoaderCompatibility(m),
752
755
  load_contract_default_profile: m.load_contract ? m.load_contract.default_profile : null,
753
756
  };
754
757
  if (m.signatures !== undefined) out.signature_count = Array.isArray(m.signatures) ? m.signatures.length : 0;
755
- if (v1.map['checksums.json']) out.checksums_present = true;
758
+ if (container.map['checksums.json']) out.checksums_present = true;
756
759
  return out;
757
760
  }
758
761
 
@@ -763,26 +766,27 @@ function buildInspectOutput(v1) {
763
766
  * reports each gate independently. Never includes trust / recommended
764
767
  * / high_quality / officially_approved as a positive claim.
765
768
  */
766
- function runValidate(v1) {
769
+ function runValidate(layout) {
767
770
  const result = {
768
771
  format_valid: true,
769
772
  schema_valid: true,
770
773
  payload_valid: true,
771
774
  checksums_valid: true,
772
775
  load_contract_valid: true,
776
+ ...assessLoaderCompatibility(layout.manifest),
773
777
  };
774
778
  const problems = [];
775
779
 
776
- // format gate — already proven by readV1Layout, but we re-state the gates
780
+ // format gate — already proven by readLayout, but we re-state the gates
777
781
  // so the report matches the spec.
778
- for (const f of V1_REQUIRED_DIR_ENTRIES) {
779
- if (!v1.map[f]) {
782
+ for (const f of REQUIRED_DIR_ENTRIES) {
783
+ if (!layout.map[f]) {
780
784
  result.format_valid = false;
781
785
  problems.push(`format: missing required entry ${f}`);
782
786
  }
783
787
  }
784
- if (v1.map.mimetype) {
785
- const mimeStr = v1.map.mimetype.toString('utf8');
788
+ if (layout.map.mimetype) {
789
+ const mimeStr = layout.map.mimetype.toString('utf8');
786
790
  if (mimeStr !== MIMETYPE) {
787
791
  result.format_valid = false;
788
792
  problems.push(`format: mimetype is not ${MIMETYPE}`);
@@ -798,7 +802,7 @@ function runValidate(v1) {
798
802
  );
799
803
  return finalizeValidate(result, problems);
800
804
  }
801
- if (!validators.manifest(v1.manifest)) {
805
+ if (!validators.manifest(layout.manifest)) {
802
806
  result.schema_valid = false;
803
807
  for (const err of validators.manifest.errors) {
804
808
  problems.push(`manifest: ${err.instancePath || '<root>'} ${err.message}`);
@@ -806,42 +810,74 @@ function runValidate(v1) {
806
810
  }
807
811
 
808
812
  // Known pre-1.0 aliases are not extension fields. Rejecting them here keeps
809
- // the current v1 validator as the single manifest authority while allowing
813
+ // the current layout validator as the single manifest authority while allowing
810
814
  // genuinely namespaced/forward-compatible manifest extensions.
811
815
  const removedAliases = {
812
- kdna_spec: 'kdna_version',
813
- format_version: 'kdna_version',
814
- spec_version: 'kdna_version',
816
+ kdna_spec: 'format_version',
817
+ kdna_version: 'format_version',
818
+ spec_version: 'format_version',
815
819
  };
816
820
  for (const [field, replacement] of Object.entries(removedAliases)) {
817
- if (Object.prototype.hasOwnProperty.call(v1.manifest, field)) {
821
+ if (Object.prototype.hasOwnProperty.call(layout.manifest, field)) {
818
822
  result.schema_valid = false;
819
823
  problems.push(`kdna.json: ${field} is not allowed. Use ${replacement}.`);
820
824
  }
821
825
  }
822
826
 
823
- // payload gate — payload.kdnab against payload-profile-v1.schema.json
827
+ // payload gate — payload.kdnab against payload-profile.schema.json
824
828
  // For actually encrypted payloads (envelopes), skip schema validation.
825
829
  let payload;
826
830
  try {
827
- payload = parsePayloadEntry("payload.kdnab", v1.map['payload.kdnab']);
831
+ payload = parsePayloadEntry("payload.kdnab", layout.map['payload.kdnab']);
828
832
  } catch (e) {
829
833
  result.payload_valid = false;
830
834
  problems.push(`payload: not valid KDNA payload (${e.message})`);
831
835
  return finalizeValidate(result, problems);
832
836
  }
833
837
 
834
- const isEncryptedPayload = payload.profile
835
- && payload.ciphertext
836
- && (v1.manifest.payload?.encrypted || v1.manifest.encryption?.encrypted_entries?.includes('payload.kdnab'));
838
+ const payloadIsEncryptedEnvelope = Boolean(payload.profile && payload.ciphertext);
839
+ const encryptedEntries = layout.manifest.encryption?.encrypted_entries;
840
+ const manifestDeclaresEncryptedPayload = Boolean(
841
+ layout.manifest.payload?.encrypted
842
+ || (Array.isArray(encryptedEntries) && encryptedEntries.includes('payload.kdnab')),
843
+ );
844
+ if (manifestDeclaresEncryptedPayload && !payloadIsEncryptedEnvelope) {
845
+ result.payload_valid = false;
846
+ problems.push('payload: manifest declares encryption but payload.kdnab is not an encrypted envelope');
847
+ }
848
+ if (payloadIsEncryptedEnvelope && !manifestDeclaresEncryptedPayload) {
849
+ result.payload_valid = false;
850
+ problems.push('payload: encrypted envelope is missing its manifest encryption declaration');
851
+ }
852
+ const isEncryptedPayload = payloadIsEncryptedEnvelope && manifestDeclaresEncryptedPayload;
837
853
 
838
854
  if (isEncryptedPayload) {
839
855
  // Encrypted payload — verify it's a proper encryption envelope, not plaintext.
840
- const encProfile = v1.manifest.encryption?.profile;
856
+ const encProfile = layout.manifest.encryption?.profile;
857
+ const encProfileVersion = layout.manifest.encryption?.profile_version;
858
+ if (!layout.manifest.encryption) {
859
+ result.payload_valid = false;
860
+ problems.push('payload: encrypted payload requires a manifest encryption declaration');
861
+ }
862
+ if (
863
+ !Array.isArray(encryptedEntries)
864
+ || encryptedEntries.length !== 1
865
+ || encryptedEntries[0] !== 'payload.kdnab'
866
+ ) {
867
+ result.payload_valid = false;
868
+ problems.push('payload: manifest encrypted_entries must declare only payload.kdnab');
869
+ }
841
870
  if (encProfile && payload.profile !== encProfile) {
842
871
  result.payload_valid = false;
843
872
  problems.push(`payload: encrypted envelope profile ${payload.profile || 'unknown'} does not match manifest encryption profile ${encProfile}`);
844
873
  }
874
+ const payloadProfileVersion = payload.profile === EXTERNAL_ENVELOPE_PROFILE
875
+ ? payload.contract_version
876
+ : payload.profile_version;
877
+ if (encProfileVersion && payloadProfileVersion !== encProfileVersion) {
878
+ result.payload_valid = false;
879
+ problems.push(`payload: encrypted envelope compatibility coordinate ${payloadProfileVersion || 'unknown'} does not match manifest encryption profile_version ${encProfileVersion}`);
880
+ }
845
881
  let hasKeyMaterial = !!payload.wrapped_key || (Array.isArray(payload.key_slots) && payload.key_slots.length > 0);
846
882
  if (payload.profile === EXTERNAL_ENVELOPE_PROFILE) {
847
883
  try {
@@ -859,7 +895,7 @@ function runValidate(v1) {
859
895
  // Encrypted payload passes payload gate if envelope is structurally valid.
860
896
  } else {
861
897
  // Plaintext payload — full schema validation.
862
- const isBundleProfile = v1.manifest.compatibility?.profile === 'bundle-profile-v1' || v1.manifest.asset_type === 'bundle';
898
+ const isBundleProfile = layout.manifest.compatibility?.profile === 'kdna.payload.bundle' || layout.manifest.asset_type === 'bundle';
863
899
  const payloadValidator = isBundleProfile ? validators.bundlePayload : validators.payload;
864
900
  if (!payloadValidator(payload)) {
865
901
  result.payload_valid = false;
@@ -870,10 +906,10 @@ function runValidate(v1) {
870
906
  }
871
907
 
872
908
  // checksums gate — checksums.json against checksums.schema.json
873
- if (v1.map['checksums.json']) {
909
+ if (layout.map['checksums.json']) {
874
910
  let checks;
875
911
  try {
876
- checks = parseJsonEntry('checksums.json', v1.map['checksums.json']);
912
+ checks = parseJsonEntry('checksums.json', layout.map['checksums.json']);
877
913
  } catch (e) {
878
914
  result.checksums_valid = false;
879
915
  problems.push(`checksums: not valid JSON (${e.message})`);
@@ -886,13 +922,13 @@ function runValidate(v1) {
886
922
  }
887
923
  // Digest matching verification — compute actual hashes and compare.
888
924
  if (checks) {
889
- result.checksums_valid = verifyDigests(checks, v1.map, problems, result);
925
+ result.checksums_valid = verifyDigests(checks, layout.map, problems, result);
890
926
  }
891
927
  }
892
928
 
893
929
  // load_contract gate — only if manifest references a load_contract block
894
- if (v1.manifest.load_contract) {
895
- const lc = v1.manifest.load_contract;
930
+ if (layout.manifest.load_contract) {
931
+ const lc = layout.manifest.load_contract;
896
932
  const validLc = _ajv.getSchema('load-contract.schema.json');
897
933
  if (validLc && !validLc(lc)) {
898
934
  result.load_contract_valid = false;
@@ -910,10 +946,13 @@ function runValidate(v1) {
910
946
 
911
947
  function verifyDigests(checksums, map, problems, result) {
912
948
  const digestMetadataPresent =
913
- checksums.digest_profile !== undefined || checksums.covered_entries !== undefined;
949
+ checksums.digest_profile !== undefined ||
950
+ checksums.digest_profile_version !== undefined ||
951
+ checksums.covered_entries !== undefined;
914
952
  if (digestMetadataPresent) {
915
953
  const requiredDigestFields = [
916
954
  'digest_profile',
955
+ 'digest_profile_version',
917
956
  'covered_entries',
918
957
  'manifest_digest',
919
958
  'payload_digest',
@@ -924,13 +963,18 @@ function verifyDigests(checksums, map, problems, result) {
924
963
  );
925
964
  if (missingDigestFields.length) {
926
965
  problems.push(
927
- `checksums: kdna-runtime-entry-set-v1 metadata requires ${missingDigestFields.join(', ')}`,
966
+ `checksums: kdna.digest-basis.runtime-entry-set metadata requires ${missingDigestFields.join(', ')}`,
928
967
  );
929
968
  result.checksums_valid = false;
930
969
  return false;
931
970
  }
932
- if (checksums.digest_profile !== 'kdna-runtime-entry-set-v1') {
933
- problems.push('checksums: unsupported digest_profile (expected kdna-runtime-entry-set-v1)');
971
+ if (checksums.digest_profile !== 'kdna.digest-basis.runtime-entry-set') {
972
+ problems.push('checksums: unsupported digest_profile (expected kdna.digest-basis.runtime-entry-set)');
973
+ result.checksums_valid = false;
974
+ return false;
975
+ }
976
+ if (checksums.digest_profile_version !== '0.1.0') {
977
+ problems.push('checksums: unsupported digest_profile_version (expected 0.1.0)');
934
978
  result.checksums_valid = false;
935
979
  return false;
936
980
  }
@@ -941,7 +985,7 @@ function verifyDigests(checksums, map, problems, result) {
941
985
  || coveredEntries[0] !== 'kdna.json'
942
986
  || coveredEntries[1] !== 'payload.kdnab'
943
987
  ) {
944
- problems.push('checksums: covered_entries does not match kdna-runtime-entry-set-v1');
988
+ problems.push('checksums: covered_entries does not match kdna.digest-basis.runtime-entry-set');
945
989
  result.checksums_valid = false;
946
990
  return false;
947
991
  }
@@ -975,21 +1019,12 @@ function verifyDigests(checksums, map, problems, result) {
975
1019
  stillValid = false;
976
1020
  }
977
1021
  }
978
- if (
979
- checksums.entry_set_digest
980
- && checksums.asset_digest
981
- && checksums.entry_set_digest !== checksums.asset_digest
982
- ) {
983
- problems.push('checksums: entry_set_digest does not match deprecated asset_digest alias');
984
- stillValid = false;
985
- }
986
- const declaredEntrySetDigest = checksums.entry_set_digest || checksums.asset_digest;
1022
+ const declaredEntrySetDigest = checksums.entry_set_digest;
987
1023
  if (declaredEntrySetDigest) {
988
- const digestLabel = checksums.entry_set_digest ? 'entry_set_digest' : 'asset_digest';
989
1024
  const expectedEntrySetDigest = declaredEntrySetDigest.replace(/^sha256:/, '');
990
1025
  const missingAssetInputs = ['kdna.json', 'payload.kdnab'].filter((entryName) => !entryDigests[entryName]);
991
1026
  if (missingAssetInputs.length) {
992
- problems.push(`checksums: ${digestLabel} cannot be verified without ${missingAssetInputs.join(', ')}`);
1027
+ problems.push(`checksums: entry_set_digest cannot be verified without ${missingAssetInputs.join(', ')}`);
993
1028
  stillValid = false;
994
1029
  } else {
995
1030
  const actualEntrySetDigest = computeRuntimeEntrySetDigest(
@@ -998,7 +1033,7 @@ function verifyDigests(checksums, map, problems, result) {
998
1033
  ).slice('sha256:'.length);
999
1034
  if (actualEntrySetDigest !== expectedEntrySetDigest) {
1000
1035
  problems.push(
1001
- `checksums: ${digestLabel} mismatch (declared ${expectedEntrySetDigest.slice(0, 8)}..., actual ${actualEntrySetDigest.slice(0, 8)}...)`,
1036
+ `checksums: entry_set_digest mismatch (declared ${expectedEntrySetDigest.slice(0, 8)}..., actual ${actualEntrySetDigest.slice(0, 8)}...)`,
1002
1037
  );
1003
1038
  stillValid = false;
1004
1039
  }
@@ -1053,7 +1088,7 @@ function computeRuntimeEntrySetDigest(manifestBytes, payloadBytes) {
1053
1088
  return `sha256:${crypto.createHash('sha256').update(combined).digest('hex')}`;
1054
1089
  }
1055
1090
 
1056
- function buildChecksumsV1(sourceDir) {
1091
+ function buildChecksums(sourceDir) {
1057
1092
  const absSrc = path.resolve(sourceDir);
1058
1093
  if (!fs.existsSync(absSrc) || !fs.statSync(absSrc).isDirectory()) {
1059
1094
  throw new Error(`not a directory: ${absSrc}`);
@@ -1068,13 +1103,13 @@ function buildChecksumsV1(sourceDir) {
1068
1103
  fs.readFileSync(path.join(absSrc, 'payload.kdnab')),
1069
1104
  );
1070
1105
  return {
1071
- digest_profile: 'kdna-runtime-entry-set-v1',
1106
+ digest_profile: 'kdna.digest-basis.runtime-entry-set',
1107
+ digest_profile_version: '0.1.0',
1072
1108
  covered_entries: ['kdna.json', 'payload.kdnab'],
1073
1109
  algorithm: 'sha256',
1074
1110
  manifest_digest: `sha256:${entries['kdna.json'].value}`,
1075
1111
  payload_digest: `sha256:${entries['payload.kdnab'].value}`,
1076
1112
  entry_set_digest: entrySetDigest,
1077
- asset_digest: entrySetDigest,
1078
1113
  entries,
1079
1114
  };
1080
1115
  }
@@ -1082,17 +1117,19 @@ function buildChecksumsV1(sourceDir) {
1082
1117
  // ─── pack ──────────────────────────────────────────────────────────────
1083
1118
 
1084
1119
  /**
1085
- * Pack an authoring source directory into a .kdna asset. Output is
1086
- * deterministic: the same source directory packed twice produces
1087
- * byte-identical output (fixed DOS timestamps, fixed entry order,
1088
- * mimetype first).
1120
+ * Pack an authoring source directory into a .kdna asset. With one pinned
1121
+ * packer toolchain and compressor, output is byte-reproducible (fixed DOS
1122
+ * timestamps, fixed entry order, mimetype first). DEFLATE output may differ
1123
+ * across compressors, zlib versions, or systems. entry_set_digest and
1124
+ * source_fingerprint identify the logical entries; A binds the exact immutable
1125
+ * package bytes.
1089
1126
  */
1090
1127
  function pack(sourceDir, outputPath) {
1091
1128
  const absSrc = path.resolve(sourceDir);
1092
1129
  if (!fs.existsSync(absSrc) || !fs.statSync(absSrc).isDirectory()) {
1093
1130
  throw new Error(`not a directory: ${absSrc}`);
1094
1131
  }
1095
- for (const f of V1_REQUIRED_DIR_ENTRIES) {
1132
+ for (const f of REQUIRED_DIR_ENTRIES) {
1096
1133
  if (!fs.existsSync(path.join(absSrc, f))) {
1097
1134
  throw new Error(`cannot pack: missing required entry ${f}`);
1098
1135
  }
@@ -1171,7 +1208,7 @@ function pack(sourceDir, outputPath) {
1171
1208
  // ─── unpack ────────────────────────────────────────────────────────────
1172
1209
 
1173
1210
  /**
1174
- * Unpack a v1 .kdna container to a directory. Refuses path traversal.
1211
+ * Unpack a layout .kdna container to a directory. Refuses path traversal.
1175
1212
  * Does not auto-execute any entry.
1176
1213
  */
1177
1214
  function unpack(inputPath, outputDir) {
@@ -1212,23 +1249,20 @@ function unpack(inputPath, outputDir) {
1212
1249
  // ─── Public router entry points ────────────────────────────────────────
1213
1250
 
1214
1251
  function inspect(inputPath, _opts = {}) {
1215
- const v1 = readInputLayout(inputPath);
1216
- const out = buildInspectOutput(v1);
1252
+ const layout = readInputLayout(inputPath);
1253
+ const out = buildInspectOutput(layout);
1217
1254
  // Guard against accidental forbidden wording in any future field additions.
1218
1255
  assertNoForbiddenTerms(out);
1219
1256
  return out;
1220
1257
  }
1221
1258
 
1222
1259
  function validate(inputPath, _opts = {}) {
1223
- const v1 = readInputLayout(inputPath);
1224
- return runValidate(v1);
1260
+ const layout = readInputLayout(inputPath);
1261
+ return runValidate(layout);
1225
1262
  }
1226
1263
 
1227
1264
  function normalizeAccess(access) {
1228
1265
  const value = access || 'public';
1229
- if (value === 'open') return { access: 'public', alias: value };
1230
- if (value === 'protected') return { access: 'licensed', alias: value };
1231
- if (value === 'runtime') return { access: 'remote', alias: value };
1232
1266
  return { access: value, alias: null };
1233
1267
  }
1234
1268
 
@@ -1236,13 +1270,12 @@ function inferEntitlementProfile(manifest) {
1236
1270
  if (manifest.entitlement && typeof manifest.entitlement.profile === 'string') {
1237
1271
  return manifest.entitlement.profile;
1238
1272
  }
1239
- if (manifest.encryption && manifest.encryption.profile === 'kdna-password-protected-v1') {
1273
+ if (manifest.encryption && manifest.encryption.profile === 'kdna.encryption.password') {
1240
1274
  return 'password';
1241
1275
  }
1242
- if (manifest.encryption && manifest.encryption.profile === 'kdna-password-protected-v1-scrypt') {
1276
+ if (manifest.encryption && manifest.encryption.profile === 'kdna.encryption.password.scrypt') {
1243
1277
  return 'password';
1244
1278
  }
1245
- if (manifest.access === 'protected') return 'password';
1246
1279
  return null;
1247
1280
  }
1248
1281
 
@@ -1264,14 +1297,14 @@ function finalizeLoadPlan(plan) {
1264
1297
  return plan;
1265
1298
  }
1266
1299
 
1267
- function computeSourceFingerprint(_inputPath, v1) {
1300
+ function computeSourceFingerprint(_inputPath, layout) {
1268
1301
  // Fingerprint the logical entries, not the transport ZIP bytes. DEFLATE
1269
1302
  // output can differ between zlib versions and operating systems even when
1270
1303
  // every uncompressed KDNA entry is identical. LoadPlan conformance must be
1271
1304
  // stable across JS, Swift, CI runners, and repackaging transports.
1272
1305
  const hash = crypto.createHash('sha256');
1273
- for (const name of Object.keys(v1.map).sort()) {
1274
- const bytes = v1.map[name];
1306
+ for (const name of Object.keys(layout.map).sort()) {
1307
+ const bytes = layout.map[name];
1275
1308
  hash.update(name);
1276
1309
  hash.update('\0');
1277
1310
  hash.update(String(bytes.length));
@@ -1282,7 +1315,7 @@ function computeSourceFingerprint(_inputPath, v1) {
1282
1315
  return `sha256:${hash.digest('hex')}`;
1283
1316
  }
1284
1317
 
1285
- function inputFingerprint(inputPath, v1, opts = {}) {
1318
+ function inputFingerprint(inputPath, layout, opts = {}) {
1286
1319
  const allowedEntitlement = new Set(['active', 'expired', 'revoked', 'offline_grace']);
1287
1320
  const entitlementInput = (
1288
1321
  opts.entitlement && allowedEntitlement.has(opts.entitlement.status)
@@ -1291,12 +1324,12 @@ function inputFingerprint(inputPath, v1, opts = {}) {
1291
1324
  return {
1292
1325
  has_password_input: opts.hasPassword === true,
1293
1326
  entitlement_input: entitlementInput,
1294
- source_fingerprint: computeSourceFingerprint(inputPath, v1),
1327
+ source_fingerprint: computeSourceFingerprint(inputPath, layout),
1295
1328
  };
1296
1329
  }
1297
1330
 
1298
- function baseLoadPlan(inputPath, v1, validation, opts = {}) {
1299
- const manifest = v1.manifest;
1331
+ function baseLoadPlan(inputPath, layout, validation, opts = {}) {
1332
+ const manifest = layout.manifest;
1300
1333
  const accessInfo = normalizeAccess(manifest.access);
1301
1334
  const entitlementProfile = inferEntitlementProfile(manifest);
1302
1335
  const asset = {
@@ -1308,7 +1341,7 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
1308
1341
  };
1309
1342
 
1310
1343
  const plan = {
1311
- kdna_version: manifest.kdna_version || null,
1344
+ format_version: manifest.format_version || null,
1312
1345
  asset,
1313
1346
  access: accessInfo.access,
1314
1347
  access_alias: accessInfo.alias,
@@ -1317,7 +1350,7 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
1317
1350
  required_action: 'block',
1318
1351
  can_load_now: false,
1319
1352
  projection_policy: 'none',
1320
- input_fingerprint: inputFingerprint(inputPath, v1, opts),
1353
+ input_fingerprint: inputFingerprint(inputPath, layout, opts),
1321
1354
  checks: {
1322
1355
  format_valid: validation.format_valid,
1323
1356
  schema_valid: validation.schema_valid,
@@ -1328,7 +1361,7 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
1328
1361
  },
1329
1362
  issues: [],
1330
1363
  source: {
1331
- kind: v1.kind,
1364
+ kind: layout.kind,
1332
1365
  path: typeof inputPath === 'string' ? path.resolve(inputPath) : null,
1333
1366
  },
1334
1367
  };
@@ -1344,9 +1377,9 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
1344
1377
  return plan;
1345
1378
  }
1346
1379
 
1347
- // ─── B1: Unified container model → legacy v1 layout ─────────────────────
1380
+ // ─── B1: Unified container model → legacy layout layout ─────────────────────
1348
1381
 
1349
- function canonicalToV1Layout(asset) {
1382
+ function canonicalToContainerLayout(asset) {
1350
1383
  const map = {};
1351
1384
  if (asset.entries) {
1352
1385
  for (const [name, buf] of asset.entries) {
@@ -1379,14 +1412,14 @@ function canonicalToV1Layout(asset) {
1379
1412
  * result instead of parsing manifest fields directly.
1380
1413
  */
1381
1414
  function planLoad(inputPath, opts = {}) {
1382
- let v1;
1415
+ let layout;
1383
1416
 
1384
1417
  if (Buffer.isBuffer(inputPath) || inputPath instanceof Uint8Array) {
1385
1418
  try {
1386
- v1 = readInputLayout(inputPath);
1419
+ layout = readInputLayout(inputPath);
1387
1420
  } catch (e) {
1388
1421
  return finalizeLoadPlan({
1389
- kdna_version: null,
1422
+ format_version: null,
1390
1423
  asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
1391
1424
  access: null, access_alias: null, entitlement_profile: null,
1392
1425
  state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
@@ -1400,17 +1433,17 @@ function planLoad(inputPath, opts = {}) {
1400
1433
 
1401
1434
  // Try the unified container dispatcher first; the internal compatibility
1402
1435
  // reader below remains available only for authoring and migration tooling.
1403
- if (!v1 && readAsset !== null) {
1436
+ if (!layout && readAsset !== null) {
1404
1437
  try {
1405
1438
  const asset = readAsset(path.resolve(inputPath));
1406
- v1 = canonicalToV1Layout(asset);
1439
+ layout = canonicalToContainerLayout(asset);
1407
1440
  } catch (e) {
1408
- // If dispatcher fails, fall back to legacy readV1Layout for backward compat
1441
+ // If dispatcher fails, fall back to legacy readLayout for backward compat
1409
1442
  if (e.code === 'KDNA_FORMAT_UNKNOWN' || e.code === 'KDNA_PATH_NOT_FOUND') {
1410
1443
  // let the legacy path handle it
1411
1444
  } else {
1412
1445
  return finalizeLoadPlan({
1413
- kdna_version: null,
1446
+ format_version: null,
1414
1447
  asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
1415
1448
  access: null, access_alias: null, entitlement_profile: null,
1416
1449
  state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
@@ -1426,12 +1459,12 @@ function planLoad(inputPath, opts = {}) {
1426
1459
  }
1427
1460
  }
1428
1461
 
1429
- if (!v1) {
1462
+ if (!layout) {
1430
1463
  try {
1431
- v1 = readInputLayout(inputPath);
1464
+ layout = readInputLayout(inputPath);
1432
1465
  } catch (e) {
1433
1466
  return finalizeLoadPlan({
1434
- kdna_version: null,
1467
+ format_version: null,
1435
1468
  asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
1436
1469
  access: null, access_alias: null, entitlement_profile: null,
1437
1470
  state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
@@ -1443,10 +1476,14 @@ function planLoad(inputPath, opts = {}) {
1443
1476
  }
1444
1477
  }
1445
1478
 
1446
- const validation = runValidate(v1);
1447
- const plan = baseLoadPlan(inputPath, v1, validation, opts);
1479
+ const validation = runValidate(layout);
1480
+ const plan = baseLoadPlan(inputPath, layout, validation, opts);
1448
1481
 
1449
1482
  if (!validation.overall_valid) {
1483
+ if (!['public', 'licensed', 'remote'].includes(plan.access)) {
1484
+ plan.access = null;
1485
+ plan.access_alias = null;
1486
+ }
1450
1487
  plan.state = 'invalid';
1451
1488
  plan.required_action = 'block';
1452
1489
  plan.can_load_now = false;
@@ -1457,22 +1494,27 @@ function planLoad(inputPath, opts = {}) {
1457
1494
  return finalizeLoadPlan(plan);
1458
1495
  }
1459
1496
 
1460
- const manifest = v1.manifest;
1497
+ const manifest = layout.manifest;
1498
+
1499
+ if (validation.loader_compatible === false) {
1500
+ plan.state = 'invalid';
1501
+ plan.required_action = 'block';
1502
+ plan.can_load_now = false;
1503
+ plan.projection_policy = 'none';
1504
+ plan.issues.push(buildLoadPlanIssue(
1505
+ 'KDNA_LOADER_VERSION_UNSUPPORTED',
1506
+ 'blocking',
1507
+ loaderVersionUnsupportedMessage(validation.min_loader_version),
1508
+ ));
1509
+ return finalizeLoadPlan(plan);
1510
+ }
1461
1511
 
1462
1512
  function externalEntitlementMatchesCurrentAsset(entitlement) {
1463
1513
  if (!isVerifiedExternalEntitlement(entitlement) || !entitlement.asset) return false;
1464
- let checksums;
1465
- try {
1466
- checksums = v1.map['checksums.json']
1467
- ? parseJsonEntry('checksums.json', v1.map['checksums.json'])
1468
- : null;
1469
- } catch {
1470
- return false;
1471
- }
1472
- return entitlement.asset.asset_id === (manifest.asset_id || manifest.name)
1514
+ return entitlement.asset.asset_id === manifest.asset_id
1473
1515
  && entitlement.asset.asset_uid === manifest.asset_uid
1474
1516
  && entitlement.asset.version === manifest.version
1475
- && entitlement.asset.digest === checksums?.asset_digest
1517
+ && entitlement.asset.digest === layout.containerDigest
1476
1518
  && entitlement.asset.entry_path === manifest.payload?.path;
1477
1519
  }
1478
1520
 
@@ -1867,35 +1909,46 @@ function assertNoForbiddenTerms(obj) {
1867
1909
  walk(obj);
1868
1910
  if (seen.size > 0) {
1869
1911
  throw new Error(
1870
- `internal: v1 inspect output contains forbidden terms: ${[...seen].join(', ')}`,
1912
+ `internal: layout inspect output contains forbidden terms: ${[...seen].join(', ')}`,
1871
1913
  );
1872
1914
  }
1873
1915
  }
1874
1916
 
1875
1917
  module.exports = {
1876
1918
  MIMETYPE,
1919
+ KDNA_LOADER_VERSION,
1877
1920
  REQUIRED_DIR_ENTRIES,
1878
1921
  isKdnaSourceDir,
1879
1922
  detectContainerFormat,
1880
- readLayout: readV1Layout,
1923
+ readLayout: readLayout,
1881
1924
  inspect,
1882
1925
  validate,
1883
1926
  planLoad,
1884
1927
  loadAuthorized,
1885
- buildChecksums: buildChecksumsV1,
1928
+ buildChecksums: buildChecksums,
1886
1929
  computeRuntimeEntrySetDigest,
1887
1930
  pack,
1888
1931
  unpack,
1889
- load: loadV1,
1890
- loadAsset: loadV1,
1891
- buildCapsule,
1932
+ load: loadAsset,
1933
+ loadAsset: loadAsset,
1892
1934
  FORBIDDEN_OUTPUT_TERMS,
1893
1935
  parseSemver,
1894
1936
  compareSemver,
1895
1937
  satisfies,
1896
1938
  };
1897
1939
 
1898
- // ─── loadV1 — v1 runtime loading / load contract ──────────────────────
1940
+ // The remote Runtime package subpath uses this non-enumerable bridge. Keeping
1941
+ // it non-enumerable prevents the package-root object spread from turning the
1942
+ // deployer-only contract into an ordinary consumer API. Package exports also
1943
+ // block direct access to this internal container module after installation.
1944
+ Object.defineProperty(module.exports, 'loadRemoteRuntimeAssetForServer', {
1945
+ value: loadRemoteRuntimeAssetForServer,
1946
+ enumerable: false,
1947
+ configurable: false,
1948
+ writable: false,
1949
+ });
1950
+
1951
+ // ─── loadAsset — layout runtime loading / load contract ──────────────────────
1899
1952
 
1900
1953
  function renderPromptItem(item) {
1901
1954
  if (item === undefined || item === null) return '';
@@ -1916,7 +1969,7 @@ function renderPromptItem(item) {
1916
1969
  // Boundary card: kdna-studio writes { id, scope, out_of_scope,
1917
1970
  // acceptable_exceptions }. Render the scope/out-of-scope rule
1918
1971
  // instead of falling through to the UUID `id` (which was the
1919
- // v0.15.0 behavior and made boundaries unreadable in
1972
+ // 0.15.0 behavior and made boundaries unreadable in
1920
1973
  // `kdna load --as=prompt`).
1921
1974
  if (
1922
1975
  item.type === 'boundary' ||
@@ -1979,6 +2032,7 @@ function loadAuthorized(inputPath, opts = {}) {
1979
2032
  // A packaged path is read exactly once. Authorization and projection must
1980
2033
  // operate on the same immutable byte snapshot even if the path changes
1981
2034
  // while this synchronous call is in progress.
2035
+ const inputKind = typeof inputPath === 'string' ? 'packaged_file' : 'packaged_bytes';
1982
2036
  const inputSnapshot = snapshotPackagedInput(inputPath);
1983
2037
  const plan = planLoad(inputSnapshot, opts);
1984
2038
  if (plan.can_load_now !== true) {
@@ -2000,11 +2054,155 @@ function loadAuthorized(inputPath, opts = {}) {
2000
2054
  schema_valid: plan.checks && plan.checks.overall_valid === true,
2001
2055
  signature_valid: plan.signature_valid === true,
2002
2056
  },
2057
+ _runtimeAssetBytes: inputSnapshot,
2058
+ _runtimeInputKind: inputKind,
2003
2059
  };
2004
- return loadV1Unsafe(inputSnapshot, mergedOpts);
2060
+ return loadAssetUnsafe(inputSnapshot, mergedOpts);
2061
+ }
2062
+
2063
+ function remoteRuntimeDeniedPlan(plan, code, message) {
2064
+ return finalizeLoadPlan({
2065
+ ...plan,
2066
+ state: 'invalid',
2067
+ required_action: 'block',
2068
+ can_load_now: false,
2069
+ projection_policy: 'none',
2070
+ issues: [buildLoadPlanIssue(code, 'blocking', message)],
2071
+ });
2005
2072
  }
2006
2073
 
2007
- function loadV1(inputPath, opts = {}) {
2074
+ function packagedAssetRequiredPlan(inputPath) {
2075
+ return finalizeLoadPlan({
2076
+ format_version: null,
2077
+ asset: {
2078
+ asset_id: null,
2079
+ asset_uid: null,
2080
+ title: null,
2081
+ version: null,
2082
+ judgment_version: null,
2083
+ },
2084
+ access: null,
2085
+ access_alias: null,
2086
+ entitlement_profile: null,
2087
+ state: 'invalid',
2088
+ required_action: 'block',
2089
+ can_load_now: false,
2090
+ projection_policy: 'none',
2091
+ input_fingerprint: null,
2092
+ checks: {
2093
+ format_valid: false,
2094
+ schema_valid: false,
2095
+ payload_valid: false,
2096
+ checksums_valid: false,
2097
+ load_contract_valid: false,
2098
+ overall_valid: false,
2099
+ },
2100
+ issues: [buildLoadPlanIssue(
2101
+ 'KDNA_ASSET_FILE_REQUIRED',
2102
+ 'blocking',
2103
+ 'Remote Runtime loading requires a packaged .kdna asset file. Source directories are authoring inputs only.',
2104
+ )],
2105
+ source: {
2106
+ kind: 'dir',
2107
+ path: typeof inputPath === 'string' ? path.resolve(inputPath) : null,
2108
+ },
2109
+ });
2110
+ }
2111
+
2112
+ function loadPlanDeniedError(plan, fallbackCode = 'KDNA_LOAD_NOT_AUTHORIZED') {
2113
+ const issueCodes = Array.isArray(plan && plan.issues)
2114
+ ? plan.issues.map((issue) => issue.code).filter(Boolean)
2115
+ : [];
2116
+ const error = new Error(
2117
+ `LoadPlan denied loading: state=${plan?.state || 'invalid'} required_action=${plan?.required_action || 'block'}`,
2118
+ );
2119
+ error.code = issueCodes[0] || fallbackCode;
2120
+ error.plan = plan;
2121
+ return error;
2122
+ }
2123
+
2124
+ /**
2125
+ * Deployer-only remote Runtime loading contract.
2126
+ *
2127
+ * The caller must already control the packaged remote asset. This function
2128
+ * snapshots a packaged path exactly once, validates and plans that immutable
2129
+ * byte snapshot, accepts only the remote access mode and the frozen
2130
+ * single-asset shape, then emits the full JSON Runtime Capsule required by a
2131
+ * server-side projection engine.
2132
+ */
2133
+ function loadRemoteRuntimeAssetForServer(inputPath) {
2134
+ const inputKind = typeof inputPath === 'string' ? 'packaged_file' : 'packaged_bytes';
2135
+ const inputSnapshot = snapshotPackagedInput(inputPath);
2136
+
2137
+ if (!Buffer.isBuffer(inputSnapshot)) {
2138
+ if (typeof inputPath === 'string') {
2139
+ try {
2140
+ if (fs.statSync(path.resolve(inputPath)).isDirectory()) {
2141
+ throw loadPlanDeniedError(packagedAssetRequiredPlan(inputPath));
2142
+ }
2143
+ } catch (error) {
2144
+ if (error && error.code === 'KDNA_ASSET_FILE_REQUIRED') throw error;
2145
+ }
2146
+ throw loadPlanDeniedError(planLoad(inputPath));
2147
+ }
2148
+ const plan = remoteRuntimeDeniedPlan(
2149
+ packagedAssetRequiredPlan(null),
2150
+ 'KDNA_FORMAT_INVALID',
2151
+ 'Remote Runtime input must be a packaged .kdna file path or packaged bytes.',
2152
+ );
2153
+ throw loadPlanDeniedError(plan);
2154
+ }
2155
+
2156
+ const plan = planLoad(inputSnapshot);
2157
+ if (!plan.checks || plan.checks.overall_valid !== true) {
2158
+ throw loadPlanDeniedError(plan);
2159
+ }
2160
+
2161
+ if (plan.access !== 'remote') {
2162
+ throw loadPlanDeniedError(remoteRuntimeDeniedPlan(
2163
+ plan,
2164
+ 'KDNA_REMOTE_RUNTIME_ACCESS_REQUIRED',
2165
+ 'The Remote Runtime loader accepts only packaged assets whose access mode is remote.',
2166
+ ));
2167
+ }
2168
+
2169
+ const layout = readInputLayout(inputSnapshot);
2170
+ const manifest = layout.manifest;
2171
+ const hasDependencies = manifest.dependencies
2172
+ && typeof manifest.dependencies === 'object'
2173
+ && Object.keys(manifest.dependencies).length > 0;
2174
+ if (hasDependencies || manifest.extends !== undefined) {
2175
+ throw loadPlanDeniedError(remoteRuntimeDeniedPlan(
2176
+ plan,
2177
+ 'KDNA_REMOTE_RUNTIME_COMPOSITION_UNSUPPORTED',
2178
+ 'Remote Runtime loading currently accepts one asset only; dependencies and extends declarations are not authorized.',
2179
+ ));
2180
+ }
2181
+
2182
+ if (
2183
+ plan.state !== 'needs_runtime'
2184
+ || plan.required_action !== 'connect_runtime'
2185
+ || plan.can_load_now !== false
2186
+ || plan.projection_policy !== 'remote'
2187
+ ) {
2188
+ throw loadPlanDeniedError(plan);
2189
+ }
2190
+
2191
+ return loadAssetUnsafe(inputSnapshot, {
2192
+ profile: 'full',
2193
+ as: 'json',
2194
+ resolvedDependencies: [],
2195
+ extendsChain: [],
2196
+ _validation: {
2197
+ schema_valid: plan.checks.overall_valid === true,
2198
+ signature_valid: plan.signature_valid === true,
2199
+ },
2200
+ _runtimeAssetBytes: inputSnapshot,
2201
+ _runtimeInputKind: inputKind,
2202
+ });
2203
+ }
2204
+
2205
+ function loadAsset(inputPath, opts = {}) {
2008
2206
  return loadAuthorized(inputPath, opts);
2009
2207
  }
2010
2208
 
@@ -2068,15 +2266,15 @@ function hasCompactPromptContent(content) {
2068
2266
  );
2069
2267
  }
2070
2268
 
2071
- function loadV1Unsafe(inputPath, opts = {}) {
2072
- const v1 = readInputLayout(inputPath);
2073
- const m = v1.manifest;
2269
+ function loadAssetUnsafe(inputPath, opts = {}) {
2270
+ const layout = readInputLayout(inputPath);
2271
+ const m = layout.manifest;
2074
2272
  const profile = opts.profile || (m.load_contract ? m.load_contract.default_profile : 'compact') || 'compact';
2075
2273
  const as = opts.as || 'json';
2076
2274
 
2077
2275
  let payload;
2078
2276
  try {
2079
- const rawPayload = v1.map['payload.kdnab'];
2277
+ const rawPayload = layout.map['payload.kdnab'];
2080
2278
  payload = parsePayloadEntry("payload.kdnab", rawPayload);
2081
2279
  } catch (e) {
2082
2280
  throw new Error(`payload.kdnab is not valid KDNA payload: ${e.message}`, { cause: e });
@@ -2088,10 +2286,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
2088
2286
  // Consumer-provided decrypt hook (e.g., licensed entry key)
2089
2287
  try {
2090
2288
  const decrypted = opts.decryptEntry({
2091
- asset: { entries: v1.map, manifest: m },
2289
+ asset: { entries: layout.map, manifest: m },
2092
2290
  manifest: m,
2093
2291
  entryName: 'payload.kdnab',
2094
- ciphertext: v1.map['payload.kdnab'],
2292
+ ciphertext: layout.map['payload.kdnab'],
2095
2293
  });
2096
2294
  const decryptedBuf = typeof decrypted === 'string'
2097
2295
  ? Buffer.from(decrypted, 'utf8')
@@ -2103,7 +2301,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
2103
2301
  throw err;
2104
2302
  }
2105
2303
  } else if (opts.password) {
2106
- // Password-based decryption via kdna-password-protected-v1*
2304
+ // Password-based decryption via kdna.encryption.password*
2107
2305
  // profiles. Detects Argon2id vs scrypt from the envelope profile.
2108
2306
  try {
2109
2307
  const {
@@ -2111,10 +2309,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
2111
2309
  decryptProtectedEntryScrypt,
2112
2310
  PASSWORD_PROTECTED_SCRYPT_PROFILE,
2113
2311
  } = require('../crypto-profile.js');
2114
- const encryptedEnvelope = v1.map['payload.kdnab'];
2312
+ const encryptedEnvelope = layout.map['payload.kdnab'];
2115
2313
  let envelope;
2116
2314
  try { envelope = cbor.decode(Buffer.isBuffer(encryptedEnvelope) ? encryptedEnvelope : Buffer.from(encryptedEnvelope)); }
2117
- catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (kdna_version 1.0)'); }
2315
+ catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (format 0.1.0)'); }
2118
2316
  const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
2119
2317
  ? decryptProtectedEntryScrypt
2120
2318
  : decryptProtectedEntry;
@@ -2142,11 +2340,11 @@ function loadV1Unsafe(inputPath, opts = {}) {
2142
2340
  }
2143
2341
 
2144
2342
  // Digest verification — refuse to load if checksums.json is present and digests mismatch.
2145
- if (v1.map['checksums.json']) {
2343
+ if (layout.map['checksums.json']) {
2146
2344
  try {
2147
- const checks = parseJsonEntry('checksums.json', v1.map['checksums.json']);
2345
+ const checks = parseJsonEntry('checksums.json', layout.map['checksums.json']);
2148
2346
  const problems = [];
2149
- const ok = verifyDigests(checks, v1.map, problems, {});
2347
+ const ok = verifyDigests(checks, layout.map, problems, {});
2150
2348
  if (!ok) {
2151
2349
  const err = new Error(`checksum verification failed: ${problems.join('; ')}`);
2152
2350
  err.code = 'checksum_mismatch';
@@ -2194,7 +2392,8 @@ function loadV1Unsafe(inputPath, opts = {}) {
2194
2392
  if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.compact && m.load_contract.profiles.compact.max_tokens_hint) {
2195
2393
  maxTokensHint = m.load_contract.profiles.compact.max_tokens_hint;
2196
2394
  }
2197
- result.content = { asset_id: m.asset_id, asset_uid: m.asset_uid, title: m.title, version: m.version, judgment_version: m.judgment_version, asset_type: m.asset_type, summary: m.summary || null, language: m.language || null, keywords: m.keywords || [], profiles_available: m.load_contract ? Object.keys(m.load_contract.profiles || {}) : [], max_tokens_hint: maxTokensHint };
2395
+ result.content = { asset_id: m.asset_id, asset_uid: m.asset_uid, title: m.title, version: m.version, judgment_version: m.judgment_version, asset_type: m.asset_type, summary: m.summary || null, language: m.language || null, keywords: m.keywords || [], profiles_available: m.load_contract ? Object.keys(m.load_contract.profiles || {}) : [] };
2396
+ if (maxTokensHint !== undefined) result.content.max_tokens_hint = maxTokensHint;
2198
2397
  } else if (profile === 'compact') {
2199
2398
  const core = payload.core || {};
2200
2399
  const normalizeList = (items) => (items || []).map((item) => {
@@ -2202,6 +2401,13 @@ function loadV1Unsafe(inputPath, opts = {}) {
2202
2401
  if (item && typeof item === 'object') return item;
2203
2402
  return null;
2204
2403
  }).filter(Boolean);
2404
+ const preserveDeclaredList = (items) => (
2405
+ Array.isArray(items)
2406
+ ? items.map((item) => (
2407
+ typeof item === 'string' ? item : globalThis.structuredClone(item)
2408
+ ))
2409
+ : []
2410
+ );
2205
2411
  result.content = {
2206
2412
  highest_question: core.highest_question || null,
2207
2413
  // These scoped domain-level values are already validated by the payload
@@ -2217,11 +2423,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
2217
2423
  : null,
2218
2424
  axioms: (core.axioms || []).map(normalizeCompactAxiom).filter(Boolean),
2219
2425
  boundaries: normalizeList(core.boundaries),
2220
- // The canonical payload field is singular (`reasoning.self_check`).
2221
- // Runtime Capsule uses plural (`context.self_checks`) as its projection
2222
- // field. Reading the plural name from the payload silently discarded
2223
- // every Studio-authored self-check.
2224
- self_checks: normalizeList(payload.reasoning && payload.reasoning.self_check),
2426
+ // Payload uses the singular field name while Runtime Capsule uses the
2427
+ // plural collection name. Preserve every validated item and its shape;
2428
+ // strings remain strings and structured questions remain objects.
2429
+ self_checks: preserveDeclaredList(payload.reasoning && payload.reasoning.self_check),
2225
2430
  failure_modes: normalizeList(payload.reasoning && payload.reasoning.failure_modes),
2226
2431
  patterns: normalizeList(payload.patterns).slice(0, 3),
2227
2432
  };
@@ -2243,7 +2448,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
2243
2448
  if (opts.extendsChain && opts.extendsChain.length > 0 && result.content) {
2244
2449
  try {
2245
2450
  const base = opts.extendsChain[0];
2246
- const baseLoaded = loadV1Unsafe(base.path, {
2451
+ const baseLoaded = loadAssetUnsafe(base.path, {
2247
2452
  ...opts,
2248
2453
  resolvedDependencies: [],
2249
2454
  extendsChain: [],
@@ -2287,7 +2492,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
2287
2492
 
2288
2493
  if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
2289
2494
  result.resolved_dependencies = opts.resolvedDependencies.map(dep => {
2290
- const depLoaded = loadV1Unsafe(dep.path, { ...opts, resolvedDependencies: [] });
2495
+ const depLoaded = loadAssetUnsafe(dep.path, { ...opts, resolvedDependencies: [] });
2291
2496
  // RAG namespace (Story 11): scoped identifier for namespace isolation.
2292
2497
  // Format: name@version (or just name when version is absent).
2293
2498
  const ragNamespace = dep.name
@@ -2373,7 +2578,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
2373
2578
  let textOut = text.trim();
2374
2579
  if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
2375
2580
  for (const dep of opts.resolvedDependencies) {
2376
- const depLoaded = loadV1Unsafe(dep.path, { ...opts, resolvedDependencies: [] });
2581
+ const depLoaded = loadAssetUnsafe(dep.path, { ...opts, resolvedDependencies: [] });
2377
2582
  if (depLoaded.text) {
2378
2583
  // Namespace header (Story 11): each component section is prefixed
2379
2584
  // with [NAMESPACE: id] so consumers can isolate RAG content per source.
@@ -2392,55 +2597,37 @@ function loadV1Unsafe(inputPath, opts = {}) {
2392
2597
  return result;
2393
2598
  }
2394
2599
 
2395
- return wrapAsCapsule(result, v1, profile, { ...opts, as });
2600
+ return wrapAsCapsule(result, layout, profile, { ...opts, as });
2396
2601
  }
2397
2602
 
2398
- function wrapAsCapsule(result, v1, profile, opts) {
2603
+ function wrapAsCapsule(result, layout, profile, opts) {
2399
2604
  if (opts.as === 'json') {
2400
- return buildCapsule(result, v1, profile, opts);
2605
+ return buildRuntimeCapsuleProjection(result, layout, profile, opts);
2401
2606
  }
2402
2607
  return result;
2403
2608
  }
2404
2609
 
2405
- function buildCapsule(loadResult, v1, profile, opts = {}) {
2406
- const m = v1.manifest;
2610
+ function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
2611
+ const m = layout.manifest;
2407
2612
  const val = opts._validation || {};
2408
- const assetDigest = computeRuntimeEntrySetDigest(
2409
- v1.map && v1.map['kdna.json'],
2410
- v1.map && v1.map['payload.kdnab'],
2411
- );
2412
-
2413
- const pubkey = (m.creator && m.creator.pubkey) || (m.author && m.author.pubkey) || null;
2613
+ const pubkey = (m.creator && m.creator.pubkey) || null;
2414
2614
  const sigVerified = val.signature_valid === true;
2415
- const capsule = {
2416
- type: 'kdna.context.capsule',
2417
- version: '1.0',
2418
- domain: m.name || m.asset_id || null,
2419
- judgment_version: m.judgment_version || null,
2420
- asset_digest: assetDigest,
2615
+ const runtimeCapsule = require('../runtime-capsule');
2616
+ const digests = runtimeCapsule.computeDigestEvidence(opts._runtimeAssetBytes, {
2617
+ expectedDigests: opts.expectedDigests,
2618
+ });
2619
+ return runtimeCapsule.buildRuntimeCapsule({
2620
+ projection: loadResult,
2621
+ manifest: m,
2622
+ digests,
2421
2623
  signature: pubkey
2422
2624
  ? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
2423
2625
  : { state: 'absent' },
2424
- access: m.access || 'public',
2425
- risk_level: m.risk_level || null,
2426
- profile: loadResult.profile,
2427
- context: loadResult.content || {},
2428
- trace: {
2429
- payload_encoding: (m.payload && m.payload.encoding) || 'cbor',
2430
- loaded_by: 'kdna-core',
2431
- loaded_at: new Date().toISOString(),
2432
- schema_valid: val.schema_valid === true,
2433
- signature_state: sigVerified ? 'verified' : (pubkey ? 'not_checked' : 'absent'),
2434
- profile: profile,
2435
- },
2436
- };
2437
-
2438
- if (loadResult.extends_chain) capsule.extends_chain = loadResult.extends_chain;
2439
- if (loadResult.inheritance_applied !== undefined) capsule.inheritance_applied = loadResult.inheritance_applied;
2440
- if (loadResult.resolved_dependencies) capsule.resolved_dependencies = loadResult.resolved_dependencies;
2441
- if (loadResult.rag_isolation_policy) capsule.rag_isolation_policy = loadResult.rag_isolation_policy;
2442
-
2443
- return capsule;
2626
+ inputKind: opts._runtimeInputKind,
2627
+ loadedAt: opts.loadedAt,
2628
+ schemaValid: val.schema_valid === true,
2629
+ profile,
2630
+ });
2444
2631
  }
2445
2632
 
2446
2633
  // ─── Semver & Dependency Resolution (Story 6) ─────────────────────────
@@ -2456,9 +2643,9 @@ function parseSemver(v) {
2456
2643
  };
2457
2644
  }
2458
2645
 
2459
- function compareSemver(v1, v2) {
2460
- const p1 = parseSemver(v1);
2461
- const p2 = parseSemver(v2);
2646
+ function compareSemver(leftVersion, rightVersion) {
2647
+ const p1 = parseSemver(leftVersion);
2648
+ const p2 = parseSemver(rightVersion);
2462
2649
  if (!p1 || !p2) return 0;
2463
2650
  if (p1.major !== p2.major) return p1.major - p2.major;
2464
2651
  if (p1.minor !== p2.minor) return p1.minor - p2.minor;