@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.
- package/CHANGELOG.md +227 -64
- package/README.md +114 -77
- package/package.json +8 -2
- package/schema/agent-host-capabilities.schema.json +62 -0
- package/schema/agent-host-receipt.schema.json +371 -0
- package/schema/agent-host-request.schema.json +223 -0
- package/schema/{bundle-profile-v1.schema.json → bundle-profile.schema.json} +9 -5
- package/schema/checksums.schema.json +42 -3
- package/schema/consumption-plan.schema.json +428 -0
- package/schema/digest-evidence.schema.json +212 -0
- package/schema/{kdna-envelope-external-grant-v1.schema.json → external-grant-envelope.schema.json} +17 -4
- package/schema/{kdna-key-grant-v1.schema.json → external-key-grant.schema.json} +39 -7
- package/schema/judgment-trace.schema.json +1251 -0
- package/schema/load-plan.schema.json +7 -5
- package/schema/manifest.schema.json +70 -10
- package/schema/{payload-profile-v1.schema.json → payload-profile.schema.json} +30 -6
- package/schema/runtime-capsule.schema.json +116 -0
- package/src/asset-reader.js +106 -120
- package/src/{v1 → container}/index.js +566 -209
- package/src/container/index.mjs +20 -0
- package/src/container-dispatcher.js +10 -7
- package/src/crypto-profile.js +37 -107
- package/src/external-key-grant.js +41 -15
- package/src/index.js +11 -4
- package/src/index.mjs +143 -24
- package/src/loader-compatibility.js +59 -0
- package/src/loader.js +1 -1
- package/src/public-api.js +59 -163
- package/src/remote-runtime.d.ts +13 -0
- package/src/remote-runtime.js +27 -0
- package/src/remote-runtime.mjs +3 -0
- package/src/runtime-api.js +9 -10
- package/src/runtime-capsule.js +529 -0
- package/src/runtime-contract.js +1882 -0
- package/src/types.d.ts +1127 -136
- package/src/workpack-pure.js +1 -1
- package/src/lint-pure.js +0 -430
- package/src/v1/index.mjs +0 -20
|
@@ -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
|
|
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
|
|
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
|
|
107
|
-
bundlePayloadSchema: require('../../schema/bundle-profile
|
|
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
|
|
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
|
|
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
|
|
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');
|
|
@@ -228,9 +230,13 @@ function detectContainerFormat(absPath) {
|
|
|
228
230
|
* truncated input.
|
|
229
231
|
*/
|
|
230
232
|
function listZipEntries(absPath, opts = {}) {
|
|
233
|
+
return listZipEntriesFromBuffer(fs.readFileSync(absPath), opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function listZipEntriesFromBuffer(input, opts = {}) {
|
|
231
237
|
const secure = opts.secure !== false;
|
|
232
238
|
const limits = { ...DEFAULT_CONTAINER_LIMITS, ...(opts.limits || {}) };
|
|
233
|
-
const buf =
|
|
239
|
+
const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
|
234
240
|
if (secure && buf.length > limits.maxContainerBytes) {
|
|
235
241
|
throw new Error(`container exceeds maximum size (${limits.maxContainerBytes} bytes)`);
|
|
236
242
|
}
|
|
@@ -393,8 +399,8 @@ function validateContainerEntryMetadata(entry) {
|
|
|
393
399
|
throw new Error(`container has duplicate entry: ${normalizedName}`);
|
|
394
400
|
}
|
|
395
401
|
const topLevel = normalizedName.split('/')[0];
|
|
396
|
-
if (!
|
|
397
|
-
if (
|
|
402
|
+
if (!ALLOWED_TOP_LEVEL_ENTRIES.has(topLevel)) {
|
|
403
|
+
if (FORBIDDEN_LEGACY_TOP_LEVEL.has(topLevel)) {
|
|
398
404
|
throw new Error(`container includes forbidden top-level source entry: ${topLevel}`);
|
|
399
405
|
}
|
|
400
406
|
throw new Error(`container includes unsupported top-level entry: ${topLevel}`);
|
|
@@ -512,17 +518,17 @@ function listSourceDir(dir, opts = {}) {
|
|
|
512
518
|
return out;
|
|
513
519
|
}
|
|
514
520
|
|
|
515
|
-
// ─── Read
|
|
521
|
+
// ─── Read layout from either source dir or container ───────────────────────
|
|
516
522
|
|
|
517
523
|
/**
|
|
518
|
-
* Read a
|
|
524
|
+
* Read a layout layout (source dir or .kdna container) and return a single
|
|
519
525
|
* normalized map of { mimetype, kdna.json, payload.kdnab, checksums.json? }.
|
|
520
526
|
* `where` describes the origin for error messages.
|
|
521
527
|
*
|
|
522
528
|
* Throws an Error with a clear, content-neutral message if the layout
|
|
523
529
|
* is malformed (missing entry, wrong mimetype, etc.).
|
|
524
530
|
*/
|
|
525
|
-
function
|
|
531
|
+
function readLayout(absPath) {
|
|
526
532
|
let stat;
|
|
527
533
|
try {
|
|
528
534
|
stat = fs.statSync(absPath);
|
|
@@ -533,10 +539,11 @@ function readV1Layout(absPath) {
|
|
|
533
539
|
const map = {};
|
|
534
540
|
let entries = null; // ZIP entries if container
|
|
535
541
|
let kind; // 'dir' | 'file'
|
|
542
|
+
let containerBytes = null;
|
|
536
543
|
|
|
537
544
|
if (stat.isDirectory()) {
|
|
538
545
|
kind = 'dir';
|
|
539
|
-
for (const f of
|
|
546
|
+
for (const f of REQUIRED_DIR_ENTRIES) {
|
|
540
547
|
const full = path.join(absPath, f);
|
|
541
548
|
if (!fs.existsSync(full)) {
|
|
542
549
|
throw new Error(`not a KDNA authoring source directory: missing ${f}`);
|
|
@@ -545,7 +552,7 @@ function readV1Layout(absPath) {
|
|
|
545
552
|
throw new Error(`not a KDNA authoring source directory: ${f} must not be a symlink`);
|
|
546
553
|
}
|
|
547
554
|
}
|
|
548
|
-
for (const f of [...
|
|
555
|
+
for (const f of [...REQUIRED_DIR_ENTRIES, ...OPTIONAL_DIR_ENTRIES]) {
|
|
549
556
|
const full = path.join(absPath, f);
|
|
550
557
|
if (fs.existsSync(full)) {
|
|
551
558
|
if (fs.statSync(full).isFile()) {
|
|
@@ -558,13 +565,10 @@ function readV1Layout(absPath) {
|
|
|
558
565
|
}
|
|
559
566
|
} else if (stat.isFile()) {
|
|
560
567
|
kind = 'file';
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
if (entries[0].method !== 0) {
|
|
566
|
-
throw new Error('not a KDNA Asset Container: mimetype must be uncompressed');
|
|
567
|
-
}
|
|
568
|
+
// Open once so parsing and the whole-file digest come from the same
|
|
569
|
+
// immutable snapshot, without a path-level TOCTOU window.
|
|
570
|
+
containerBytes = fs.readFileSync(absPath);
|
|
571
|
+
entries = listZipEntriesFromBuffer(containerBytes);
|
|
568
572
|
for (const e of entries) {
|
|
569
573
|
// We only need the well-known entries; signatures/ attachments/ etc.
|
|
570
574
|
// are passed through unchanged by the loader but not parsed here.
|
|
@@ -577,25 +581,36 @@ function readV1Layout(absPath) {
|
|
|
577
581
|
map[e.name] = e.data;
|
|
578
582
|
}
|
|
579
583
|
}
|
|
580
|
-
for (const f of V1_REQUIRED_DIR_ENTRIES) {
|
|
581
|
-
if (!map[f]) {
|
|
582
|
-
throw new Error(`not a KDNA Asset Container: missing ${f}`);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
584
|
} else {
|
|
586
585
|
throw new Error(`not a file or directory: ${absPath}`);
|
|
587
586
|
}
|
|
588
587
|
|
|
589
|
-
|
|
588
|
+
const containerDigest = kind === 'file'
|
|
589
|
+
? `sha256:${crypto.createHash('sha256').update(containerBytes).digest('hex')}`
|
|
590
|
+
: null;
|
|
591
|
+
|
|
592
|
+
return finalizeContainerLayout({ kind, map, entries, containerDigest });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function finalizeContainerLayout({ kind, map, entries, containerDigest }) {
|
|
596
|
+
if (kind !== 'dir' && (!entries || entries.length === 0 || entries[0].name !== 'mimetype')) {
|
|
597
|
+
throw new Error('not a KDNA Asset Container: first entry is not mimetype');
|
|
598
|
+
}
|
|
599
|
+
if (kind !== 'dir' && entries[0].method !== 0) {
|
|
600
|
+
throw new Error('not a KDNA Asset Container: mimetype must be uncompressed');
|
|
601
|
+
}
|
|
602
|
+
for (const required of REQUIRED_DIR_ENTRIES) {
|
|
603
|
+
if (!map[required]) {
|
|
604
|
+
const source = kind === 'dir' ? 'KDNA authoring source directory' : 'KDNA Asset Container';
|
|
605
|
+
throw new Error(`not a ${source}: missing ${required}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
590
609
|
const mime = map.mimetype.toString('utf8');
|
|
591
610
|
if (mime !== MIMETYPE) {
|
|
592
|
-
throw new Error(
|
|
593
|
-
`not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`,
|
|
594
|
-
);
|
|
611
|
+
throw new Error(`not a KDNA layout: mimetype is "${mime}", expected "${MIMETYPE}"`);
|
|
595
612
|
}
|
|
596
613
|
|
|
597
|
-
// Lineage must be a single object, not an array. (Format rule from
|
|
598
|
-
// docs/core/manifest.md / schema/manifest.schema.json.)
|
|
599
614
|
let manifest;
|
|
600
615
|
try {
|
|
601
616
|
manifest = parseJsonEntry('kdna.json', map['kdna.json']);
|
|
@@ -606,7 +621,60 @@ function readV1Layout(absPath) {
|
|
|
606
621
|
throw new Error('kdna.json.lineage must be an object, not an array');
|
|
607
622
|
}
|
|
608
623
|
|
|
609
|
-
return { kind, map, manifest, entries };
|
|
624
|
+
return { kind, map, manifest, entries, containerDigest };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function readLayoutBytes(input) {
|
|
628
|
+
const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
|
629
|
+
const entries = listZipEntriesFromBuffer(bytes);
|
|
630
|
+
const map = {};
|
|
631
|
+
for (const entry of entries) {
|
|
632
|
+
if (
|
|
633
|
+
entry.name === 'mimetype' ||
|
|
634
|
+
entry.name === 'kdna.json' ||
|
|
635
|
+
entry.name === 'payload.kdnab' ||
|
|
636
|
+
entry.name === 'checksums.json'
|
|
637
|
+
) {
|
|
638
|
+
map[entry.name] = entry.data;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return finalizeContainerLayout({
|
|
642
|
+
kind: 'memory',
|
|
643
|
+
map,
|
|
644
|
+
entries,
|
|
645
|
+
containerDigest: `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function readInputLayout(input) {
|
|
650
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
|
|
651
|
+
return readLayoutBytes(input);
|
|
652
|
+
}
|
|
653
|
+
return readLayout(path.resolve(input));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function snapshotPackagedInput(input) {
|
|
657
|
+
if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
|
|
658
|
+
return Buffer.from(input);
|
|
659
|
+
}
|
|
660
|
+
if (typeof input !== 'string') return input;
|
|
661
|
+
const resolved = path.resolve(input);
|
|
662
|
+
let stat;
|
|
663
|
+
try {
|
|
664
|
+
stat = fs.statSync(resolved);
|
|
665
|
+
} catch {
|
|
666
|
+
return input;
|
|
667
|
+
}
|
|
668
|
+
// Authoring directories remain available to inspect/validate. Runtime
|
|
669
|
+
// wrappers reject them; this helper only freezes packaged-file loads.
|
|
670
|
+
return stat.isFile() ? fs.readFileSync(resolved) : input;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function inputSource(input, kind) {
|
|
674
|
+
return {
|
|
675
|
+
kind,
|
|
676
|
+
path: typeof input === 'string' ? path.resolve(input) : null,
|
|
677
|
+
};
|
|
610
678
|
}
|
|
611
679
|
|
|
612
680
|
function parseJsonEntry(name, bytes, opts = {}) {
|
|
@@ -658,7 +726,7 @@ function parsePayloadEntry(name, bytes, opts = {}) {
|
|
|
658
726
|
try {
|
|
659
727
|
parsed = cbor.decode(bytes);
|
|
660
728
|
} catch {
|
|
661
|
-
throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (
|
|
729
|
+
throw new Error(`${name}: not valid CBOR. payload.kdnab must be CBOR-encoded (format 0.1.0).`);
|
|
662
730
|
}
|
|
663
731
|
assertJsonWithinLimits(parsed, name, limits);
|
|
664
732
|
return parsed;
|
|
@@ -670,10 +738,10 @@ function parsePayloadEntry(name, bytes, opts = {}) {
|
|
|
670
738
|
* Print a content-neutral manifest summary. Always JSON. Never emits
|
|
671
739
|
* the words trusted / recommended / high_quality / officially_approved.
|
|
672
740
|
*/
|
|
673
|
-
function buildInspectOutput(
|
|
674
|
-
const m =
|
|
741
|
+
function buildInspectOutput(container) {
|
|
742
|
+
const m = container.manifest;
|
|
675
743
|
const out = {
|
|
676
|
-
|
|
744
|
+
format_version: m.format_version ?? null,
|
|
677
745
|
asset_id: m.asset_id ?? null,
|
|
678
746
|
asset_uid: m.asset_uid ?? null,
|
|
679
747
|
asset_type: m.asset_type ?? null,
|
|
@@ -683,10 +751,11 @@ function buildInspectOutput(v1) {
|
|
|
683
751
|
payload: m.payload ? m.payload.path : null,
|
|
684
752
|
payload_encrypted: m.payload ? m.payload.encrypted : null,
|
|
685
753
|
profile: m.compatibility ? m.compatibility.profile : null,
|
|
754
|
+
...assessLoaderCompatibility(m),
|
|
686
755
|
load_contract_default_profile: m.load_contract ? m.load_contract.default_profile : null,
|
|
687
756
|
};
|
|
688
757
|
if (m.signatures !== undefined) out.signature_count = Array.isArray(m.signatures) ? m.signatures.length : 0;
|
|
689
|
-
if (
|
|
758
|
+
if (container.map['checksums.json']) out.checksums_present = true;
|
|
690
759
|
return out;
|
|
691
760
|
}
|
|
692
761
|
|
|
@@ -697,26 +766,27 @@ function buildInspectOutput(v1) {
|
|
|
697
766
|
* reports each gate independently. Never includes trust / recommended
|
|
698
767
|
* / high_quality / officially_approved as a positive claim.
|
|
699
768
|
*/
|
|
700
|
-
function runValidate(
|
|
769
|
+
function runValidate(layout) {
|
|
701
770
|
const result = {
|
|
702
771
|
format_valid: true,
|
|
703
772
|
schema_valid: true,
|
|
704
773
|
payload_valid: true,
|
|
705
774
|
checksums_valid: true,
|
|
706
775
|
load_contract_valid: true,
|
|
776
|
+
...assessLoaderCompatibility(layout.manifest),
|
|
707
777
|
};
|
|
708
778
|
const problems = [];
|
|
709
779
|
|
|
710
|
-
// format gate — already proven by
|
|
780
|
+
// format gate — already proven by readLayout, but we re-state the gates
|
|
711
781
|
// so the report matches the spec.
|
|
712
|
-
for (const f of
|
|
713
|
-
if (!
|
|
782
|
+
for (const f of REQUIRED_DIR_ENTRIES) {
|
|
783
|
+
if (!layout.map[f]) {
|
|
714
784
|
result.format_valid = false;
|
|
715
785
|
problems.push(`format: missing required entry ${f}`);
|
|
716
786
|
}
|
|
717
787
|
}
|
|
718
|
-
if (
|
|
719
|
-
const mimeStr =
|
|
788
|
+
if (layout.map.mimetype) {
|
|
789
|
+
const mimeStr = layout.map.mimetype.toString('utf8');
|
|
720
790
|
if (mimeStr !== MIMETYPE) {
|
|
721
791
|
result.format_valid = false;
|
|
722
792
|
problems.push(`format: mimetype is not ${MIMETYPE}`);
|
|
@@ -732,35 +802,82 @@ function runValidate(v1) {
|
|
|
732
802
|
);
|
|
733
803
|
return finalizeValidate(result, problems);
|
|
734
804
|
}
|
|
735
|
-
if (!validators.manifest(
|
|
805
|
+
if (!validators.manifest(layout.manifest)) {
|
|
736
806
|
result.schema_valid = false;
|
|
737
807
|
for (const err of validators.manifest.errors) {
|
|
738
808
|
problems.push(`manifest: ${err.instancePath || '<root>'} ${err.message}`);
|
|
739
809
|
}
|
|
740
810
|
}
|
|
741
811
|
|
|
742
|
-
//
|
|
812
|
+
// Known pre-1.0 aliases are not extension fields. Rejecting them here keeps
|
|
813
|
+
// the current layout validator as the single manifest authority while allowing
|
|
814
|
+
// genuinely namespaced/forward-compatible manifest extensions.
|
|
815
|
+
const removedAliases = {
|
|
816
|
+
kdna_spec: 'format_version',
|
|
817
|
+
kdna_version: 'format_version',
|
|
818
|
+
spec_version: 'format_version',
|
|
819
|
+
};
|
|
820
|
+
for (const [field, replacement] of Object.entries(removedAliases)) {
|
|
821
|
+
if (Object.prototype.hasOwnProperty.call(layout.manifest, field)) {
|
|
822
|
+
result.schema_valid = false;
|
|
823
|
+
problems.push(`kdna.json: ${field} is not allowed. Use ${replacement}.`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// payload gate — payload.kdnab against payload-profile.schema.json
|
|
743
828
|
// For actually encrypted payloads (envelopes), skip schema validation.
|
|
744
829
|
let payload;
|
|
745
830
|
try {
|
|
746
|
-
payload = parsePayloadEntry("payload.kdnab",
|
|
831
|
+
payload = parsePayloadEntry("payload.kdnab", layout.map['payload.kdnab']);
|
|
747
832
|
} catch (e) {
|
|
748
833
|
result.payload_valid = false;
|
|
749
834
|
problems.push(`payload: not valid KDNA payload (${e.message})`);
|
|
750
835
|
return finalizeValidate(result, problems);
|
|
751
836
|
}
|
|
752
837
|
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
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;
|
|
756
853
|
|
|
757
854
|
if (isEncryptedPayload) {
|
|
758
855
|
// Encrypted payload — verify it's a proper encryption envelope, not plaintext.
|
|
759
|
-
const encProfile =
|
|
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
|
+
}
|
|
760
870
|
if (encProfile && payload.profile !== encProfile) {
|
|
761
871
|
result.payload_valid = false;
|
|
762
872
|
problems.push(`payload: encrypted envelope profile ${payload.profile || 'unknown'} does not match manifest encryption profile ${encProfile}`);
|
|
763
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
|
+
}
|
|
764
881
|
let hasKeyMaterial = !!payload.wrapped_key || (Array.isArray(payload.key_slots) && payload.key_slots.length > 0);
|
|
765
882
|
if (payload.profile === EXTERNAL_ENVELOPE_PROFILE) {
|
|
766
883
|
try {
|
|
@@ -778,7 +895,7 @@ function runValidate(v1) {
|
|
|
778
895
|
// Encrypted payload passes payload gate if envelope is structurally valid.
|
|
779
896
|
} else {
|
|
780
897
|
// Plaintext payload — full schema validation.
|
|
781
|
-
const isBundleProfile =
|
|
898
|
+
const isBundleProfile = layout.manifest.compatibility?.profile === 'kdna.payload.bundle' || layout.manifest.asset_type === 'bundle';
|
|
782
899
|
const payloadValidator = isBundleProfile ? validators.bundlePayload : validators.payload;
|
|
783
900
|
if (!payloadValidator(payload)) {
|
|
784
901
|
result.payload_valid = false;
|
|
@@ -789,10 +906,10 @@ function runValidate(v1) {
|
|
|
789
906
|
}
|
|
790
907
|
|
|
791
908
|
// checksums gate — checksums.json against checksums.schema.json
|
|
792
|
-
if (
|
|
909
|
+
if (layout.map['checksums.json']) {
|
|
793
910
|
let checks;
|
|
794
911
|
try {
|
|
795
|
-
checks = parseJsonEntry('checksums.json',
|
|
912
|
+
checks = parseJsonEntry('checksums.json', layout.map['checksums.json']);
|
|
796
913
|
} catch (e) {
|
|
797
914
|
result.checksums_valid = false;
|
|
798
915
|
problems.push(`checksums: not valid JSON (${e.message})`);
|
|
@@ -805,13 +922,13 @@ function runValidate(v1) {
|
|
|
805
922
|
}
|
|
806
923
|
// Digest matching verification — compute actual hashes and compare.
|
|
807
924
|
if (checks) {
|
|
808
|
-
result.checksums_valid = verifyDigests(checks,
|
|
925
|
+
result.checksums_valid = verifyDigests(checks, layout.map, problems, result);
|
|
809
926
|
}
|
|
810
927
|
}
|
|
811
928
|
|
|
812
929
|
// load_contract gate — only if manifest references a load_contract block
|
|
813
|
-
if (
|
|
814
|
-
const lc =
|
|
930
|
+
if (layout.manifest.load_contract) {
|
|
931
|
+
const lc = layout.manifest.load_contract;
|
|
815
932
|
const validLc = _ajv.getSchema('load-contract.schema.json');
|
|
816
933
|
if (validLc && !validLc(lc)) {
|
|
817
934
|
result.load_contract_valid = false;
|
|
@@ -828,6 +945,51 @@ function runValidate(v1) {
|
|
|
828
945
|
}
|
|
829
946
|
|
|
830
947
|
function verifyDigests(checksums, map, problems, result) {
|
|
948
|
+
const digestMetadataPresent =
|
|
949
|
+
checksums.digest_profile !== undefined ||
|
|
950
|
+
checksums.digest_profile_version !== undefined ||
|
|
951
|
+
checksums.covered_entries !== undefined;
|
|
952
|
+
if (digestMetadataPresent) {
|
|
953
|
+
const requiredDigestFields = [
|
|
954
|
+
'digest_profile',
|
|
955
|
+
'digest_profile_version',
|
|
956
|
+
'covered_entries',
|
|
957
|
+
'manifest_digest',
|
|
958
|
+
'payload_digest',
|
|
959
|
+
'entry_set_digest',
|
|
960
|
+
];
|
|
961
|
+
const missingDigestFields = requiredDigestFields.filter(
|
|
962
|
+
(field) => checksums[field] === undefined,
|
|
963
|
+
);
|
|
964
|
+
if (missingDigestFields.length) {
|
|
965
|
+
problems.push(
|
|
966
|
+
`checksums: kdna.digest-basis.runtime-entry-set metadata requires ${missingDigestFields.join(', ')}`,
|
|
967
|
+
);
|
|
968
|
+
result.checksums_valid = false;
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
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)');
|
|
978
|
+
result.checksums_valid = false;
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
const coveredEntries = checksums.covered_entries;
|
|
982
|
+
if (
|
|
983
|
+
!Array.isArray(coveredEntries)
|
|
984
|
+
|| coveredEntries.length !== 2
|
|
985
|
+
|| coveredEntries[0] !== 'kdna.json'
|
|
986
|
+
|| coveredEntries[1] !== 'payload.kdnab'
|
|
987
|
+
) {
|
|
988
|
+
problems.push('checksums: covered_entries does not match kdna.digest-basis.runtime-entry-set');
|
|
989
|
+
result.checksums_valid = false;
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
831
993
|
const algo = checksums.algorithm || 'sha256';
|
|
832
994
|
if (algo !== 'sha256') {
|
|
833
995
|
problems.push(`checksums: unsupported digest algorithm ${algo} (supported: sha256)`);
|
|
@@ -857,21 +1019,21 @@ function verifyDigests(checksums, map, problems, result) {
|
|
|
857
1019
|
stillValid = false;
|
|
858
1020
|
}
|
|
859
1021
|
}
|
|
860
|
-
|
|
861
|
-
|
|
1022
|
+
const declaredEntrySetDigest = checksums.entry_set_digest;
|
|
1023
|
+
if (declaredEntrySetDigest) {
|
|
1024
|
+
const expectedEntrySetDigest = declaredEntrySetDigest.replace(/^sha256:/, '');
|
|
862
1025
|
const missingAssetInputs = ['kdna.json', 'payload.kdnab'].filter((entryName) => !entryDigests[entryName]);
|
|
863
1026
|
if (missingAssetInputs.length) {
|
|
864
|
-
problems.push(`checksums:
|
|
1027
|
+
problems.push(`checksums: entry_set_digest cannot be verified without ${missingAssetInputs.join(', ')}`);
|
|
865
1028
|
stillValid = false;
|
|
866
1029
|
} else {
|
|
867
|
-
const
|
|
868
|
-
.
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (actualAssetDigest !== expectedAssetDigest) {
|
|
1030
|
+
const actualEntrySetDigest = computeRuntimeEntrySetDigest(
|
|
1031
|
+
map['kdna.json'],
|
|
1032
|
+
map['payload.kdnab'],
|
|
1033
|
+
).slice('sha256:'.length);
|
|
1034
|
+
if (actualEntrySetDigest !== expectedEntrySetDigest) {
|
|
873
1035
|
problems.push(
|
|
874
|
-
`checksums:
|
|
1036
|
+
`checksums: entry_set_digest mismatch (declared ${expectedEntrySetDigest.slice(0, 8)}..., actual ${actualEntrySetDigest.slice(0, 8)}...)`,
|
|
875
1037
|
);
|
|
876
1038
|
stillValid = false;
|
|
877
1039
|
}
|
|
@@ -904,7 +1066,29 @@ function digestEntry(sourceDir, entry) {
|
|
|
904
1066
|
};
|
|
905
1067
|
}
|
|
906
1068
|
|
|
907
|
-
function
|
|
1069
|
+
function computeRuntimeEntrySetDigest(manifestBytes, payloadBytes) {
|
|
1070
|
+
if (
|
|
1071
|
+
(!Buffer.isBuffer(manifestBytes) && !(manifestBytes instanceof Uint8Array))
|
|
1072
|
+
|| (!Buffer.isBuffer(payloadBytes) && !(payloadBytes instanceof Uint8Array))
|
|
1073
|
+
) {
|
|
1074
|
+
const error = new Error(
|
|
1075
|
+
'E requires raw uncompressed kdna.json and payload.kdnab bytes.',
|
|
1076
|
+
);
|
|
1077
|
+
error.code = 'KDNA_DIGEST_INPUT_INVALID';
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
1080
|
+
const entries = {
|
|
1081
|
+
'kdna.json': crypto.createHash('sha256').update(manifestBytes).digest('hex'),
|
|
1082
|
+
'payload.kdnab': crypto.createHash('sha256').update(payloadBytes).digest('hex'),
|
|
1083
|
+
};
|
|
1084
|
+
const combined = Object.keys(entries)
|
|
1085
|
+
.sort()
|
|
1086
|
+
.map((name) => `${name}:${entries[name]}`)
|
|
1087
|
+
.join('\n');
|
|
1088
|
+
return `sha256:${crypto.createHash('sha256').update(combined).digest('hex')}`;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function buildChecksums(sourceDir) {
|
|
908
1092
|
const absSrc = path.resolve(sourceDir);
|
|
909
1093
|
if (!fs.existsSync(absSrc) || !fs.statSync(absSrc).isDirectory()) {
|
|
910
1094
|
throw new Error(`not a directory: ${absSrc}`);
|
|
@@ -914,16 +1098,18 @@ function buildChecksumsV1(sourceDir) {
|
|
|
914
1098
|
'kdna.json': digestEntry(absSrc, 'kdna.json'),
|
|
915
1099
|
'payload.kdnab': digestEntry(absSrc, 'payload.kdnab'),
|
|
916
1100
|
};
|
|
917
|
-
const
|
|
918
|
-
.
|
|
919
|
-
.
|
|
920
|
-
|
|
921
|
-
|
|
1101
|
+
const entrySetDigest = computeRuntimeEntrySetDigest(
|
|
1102
|
+
fs.readFileSync(path.join(absSrc, 'kdna.json')),
|
|
1103
|
+
fs.readFileSync(path.join(absSrc, 'payload.kdnab')),
|
|
1104
|
+
);
|
|
922
1105
|
return {
|
|
1106
|
+
digest_profile: 'kdna.digest-basis.runtime-entry-set',
|
|
1107
|
+
digest_profile_version: '0.1.0',
|
|
1108
|
+
covered_entries: ['kdna.json', 'payload.kdnab'],
|
|
923
1109
|
algorithm: 'sha256',
|
|
924
1110
|
manifest_digest: `sha256:${entries['kdna.json'].value}`,
|
|
925
1111
|
payload_digest: `sha256:${entries['payload.kdnab'].value}`,
|
|
926
|
-
|
|
1112
|
+
entry_set_digest: entrySetDigest,
|
|
927
1113
|
entries,
|
|
928
1114
|
};
|
|
929
1115
|
}
|
|
@@ -931,17 +1117,19 @@ function buildChecksumsV1(sourceDir) {
|
|
|
931
1117
|
// ─── pack ──────────────────────────────────────────────────────────────
|
|
932
1118
|
|
|
933
1119
|
/**
|
|
934
|
-
* Pack an authoring source directory into a .kdna asset.
|
|
935
|
-
*
|
|
936
|
-
*
|
|
937
|
-
*
|
|
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.
|
|
938
1126
|
*/
|
|
939
1127
|
function pack(sourceDir, outputPath) {
|
|
940
1128
|
const absSrc = path.resolve(sourceDir);
|
|
941
1129
|
if (!fs.existsSync(absSrc) || !fs.statSync(absSrc).isDirectory()) {
|
|
942
1130
|
throw new Error(`not a directory: ${absSrc}`);
|
|
943
1131
|
}
|
|
944
|
-
for (const f of
|
|
1132
|
+
for (const f of REQUIRED_DIR_ENTRIES) {
|
|
945
1133
|
if (!fs.existsSync(path.join(absSrc, f))) {
|
|
946
1134
|
throw new Error(`cannot pack: missing required entry ${f}`);
|
|
947
1135
|
}
|
|
@@ -1020,7 +1208,7 @@ function pack(sourceDir, outputPath) {
|
|
|
1020
1208
|
// ─── unpack ────────────────────────────────────────────────────────────
|
|
1021
1209
|
|
|
1022
1210
|
/**
|
|
1023
|
-
* Unpack a
|
|
1211
|
+
* Unpack a layout .kdna container to a directory. Refuses path traversal.
|
|
1024
1212
|
* Does not auto-execute any entry.
|
|
1025
1213
|
*/
|
|
1026
1214
|
function unpack(inputPath, outputDir) {
|
|
@@ -1061,23 +1249,20 @@ function unpack(inputPath, outputDir) {
|
|
|
1061
1249
|
// ─── Public router entry points ────────────────────────────────────────
|
|
1062
1250
|
|
|
1063
1251
|
function inspect(inputPath, _opts = {}) {
|
|
1064
|
-
const
|
|
1065
|
-
const out = buildInspectOutput(
|
|
1252
|
+
const layout = readInputLayout(inputPath);
|
|
1253
|
+
const out = buildInspectOutput(layout);
|
|
1066
1254
|
// Guard against accidental forbidden wording in any future field additions.
|
|
1067
1255
|
assertNoForbiddenTerms(out);
|
|
1068
1256
|
return out;
|
|
1069
1257
|
}
|
|
1070
1258
|
|
|
1071
1259
|
function validate(inputPath, _opts = {}) {
|
|
1072
|
-
const
|
|
1073
|
-
return runValidate(
|
|
1260
|
+
const layout = readInputLayout(inputPath);
|
|
1261
|
+
return runValidate(layout);
|
|
1074
1262
|
}
|
|
1075
1263
|
|
|
1076
1264
|
function normalizeAccess(access) {
|
|
1077
1265
|
const value = access || 'public';
|
|
1078
|
-
if (value === 'open') return { access: 'public', alias: value };
|
|
1079
|
-
if (value === 'protected') return { access: 'licensed', alias: value };
|
|
1080
|
-
if (value === 'runtime') return { access: 'remote', alias: value };
|
|
1081
1266
|
return { access: value, alias: null };
|
|
1082
1267
|
}
|
|
1083
1268
|
|
|
@@ -1085,13 +1270,12 @@ function inferEntitlementProfile(manifest) {
|
|
|
1085
1270
|
if (manifest.entitlement && typeof manifest.entitlement.profile === 'string') {
|
|
1086
1271
|
return manifest.entitlement.profile;
|
|
1087
1272
|
}
|
|
1088
|
-
if (manifest.encryption && manifest.encryption.profile === 'kdna
|
|
1273
|
+
if (manifest.encryption && manifest.encryption.profile === 'kdna.encryption.password') {
|
|
1089
1274
|
return 'password';
|
|
1090
1275
|
}
|
|
1091
|
-
if (manifest.encryption && manifest.encryption.profile === 'kdna
|
|
1276
|
+
if (manifest.encryption && manifest.encryption.profile === 'kdna.encryption.password.scrypt') {
|
|
1092
1277
|
return 'password';
|
|
1093
1278
|
}
|
|
1094
|
-
if (manifest.access === 'protected') return 'password';
|
|
1095
1279
|
return null;
|
|
1096
1280
|
}
|
|
1097
1281
|
|
|
@@ -1113,14 +1297,14 @@ function finalizeLoadPlan(plan) {
|
|
|
1113
1297
|
return plan;
|
|
1114
1298
|
}
|
|
1115
1299
|
|
|
1116
|
-
function computeSourceFingerprint(_inputPath,
|
|
1300
|
+
function computeSourceFingerprint(_inputPath, layout) {
|
|
1117
1301
|
// Fingerprint the logical entries, not the transport ZIP bytes. DEFLATE
|
|
1118
1302
|
// output can differ between zlib versions and operating systems even when
|
|
1119
1303
|
// every uncompressed KDNA entry is identical. LoadPlan conformance must be
|
|
1120
1304
|
// stable across JS, Swift, CI runners, and repackaging transports.
|
|
1121
1305
|
const hash = crypto.createHash('sha256');
|
|
1122
|
-
for (const name of Object.keys(
|
|
1123
|
-
const bytes =
|
|
1306
|
+
for (const name of Object.keys(layout.map).sort()) {
|
|
1307
|
+
const bytes = layout.map[name];
|
|
1124
1308
|
hash.update(name);
|
|
1125
1309
|
hash.update('\0');
|
|
1126
1310
|
hash.update(String(bytes.length));
|
|
@@ -1131,7 +1315,7 @@ function computeSourceFingerprint(_inputPath, v1) {
|
|
|
1131
1315
|
return `sha256:${hash.digest('hex')}`;
|
|
1132
1316
|
}
|
|
1133
1317
|
|
|
1134
|
-
function inputFingerprint(inputPath,
|
|
1318
|
+
function inputFingerprint(inputPath, layout, opts = {}) {
|
|
1135
1319
|
const allowedEntitlement = new Set(['active', 'expired', 'revoked', 'offline_grace']);
|
|
1136
1320
|
const entitlementInput = (
|
|
1137
1321
|
opts.entitlement && allowedEntitlement.has(opts.entitlement.status)
|
|
@@ -1140,12 +1324,12 @@ function inputFingerprint(inputPath, v1, opts = {}) {
|
|
|
1140
1324
|
return {
|
|
1141
1325
|
has_password_input: opts.hasPassword === true,
|
|
1142
1326
|
entitlement_input: entitlementInput,
|
|
1143
|
-
source_fingerprint: computeSourceFingerprint(inputPath,
|
|
1327
|
+
source_fingerprint: computeSourceFingerprint(inputPath, layout),
|
|
1144
1328
|
};
|
|
1145
1329
|
}
|
|
1146
1330
|
|
|
1147
|
-
function baseLoadPlan(inputPath,
|
|
1148
|
-
const manifest =
|
|
1331
|
+
function baseLoadPlan(inputPath, layout, validation, opts = {}) {
|
|
1332
|
+
const manifest = layout.manifest;
|
|
1149
1333
|
const accessInfo = normalizeAccess(manifest.access);
|
|
1150
1334
|
const entitlementProfile = inferEntitlementProfile(manifest);
|
|
1151
1335
|
const asset = {
|
|
@@ -1157,7 +1341,7 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
|
|
|
1157
1341
|
};
|
|
1158
1342
|
|
|
1159
1343
|
const plan = {
|
|
1160
|
-
|
|
1344
|
+
format_version: manifest.format_version || null,
|
|
1161
1345
|
asset,
|
|
1162
1346
|
access: accessInfo.access,
|
|
1163
1347
|
access_alias: accessInfo.alias,
|
|
@@ -1166,7 +1350,7 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
|
|
|
1166
1350
|
required_action: 'block',
|
|
1167
1351
|
can_load_now: false,
|
|
1168
1352
|
projection_policy: 'none',
|
|
1169
|
-
input_fingerprint: inputFingerprint(inputPath,
|
|
1353
|
+
input_fingerprint: inputFingerprint(inputPath, layout, opts),
|
|
1170
1354
|
checks: {
|
|
1171
1355
|
format_valid: validation.format_valid,
|
|
1172
1356
|
schema_valid: validation.schema_valid,
|
|
@@ -1177,8 +1361,8 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
|
|
|
1177
1361
|
},
|
|
1178
1362
|
issues: [],
|
|
1179
1363
|
source: {
|
|
1180
|
-
kind:
|
|
1181
|
-
path: path.resolve(inputPath),
|
|
1364
|
+
kind: layout.kind,
|
|
1365
|
+
path: typeof inputPath === 'string' ? path.resolve(inputPath) : null,
|
|
1182
1366
|
},
|
|
1183
1367
|
};
|
|
1184
1368
|
|
|
@@ -1193,9 +1377,9 @@ function baseLoadPlan(inputPath, v1, validation, opts = {}) {
|
|
|
1193
1377
|
return plan;
|
|
1194
1378
|
}
|
|
1195
1379
|
|
|
1196
|
-
// ─── B1: Unified container model → legacy
|
|
1380
|
+
// ─── B1: Unified container model → legacy layout layout ─────────────────────
|
|
1197
1381
|
|
|
1198
|
-
function
|
|
1382
|
+
function canonicalToContainerLayout(asset) {
|
|
1199
1383
|
const map = {};
|
|
1200
1384
|
if (asset.entries) {
|
|
1201
1385
|
for (const [name, buf] of asset.entries) {
|
|
@@ -1218,6 +1402,7 @@ function canonicalToV1Layout(asset) {
|
|
|
1218
1402
|
map,
|
|
1219
1403
|
manifest: asset.manifest,
|
|
1220
1404
|
entries: Object.keys(map),
|
|
1405
|
+
containerDigest: asset.containerDigest || null,
|
|
1221
1406
|
};
|
|
1222
1407
|
}
|
|
1223
1408
|
|
|
@@ -1227,21 +1412,38 @@ function canonicalToV1Layout(asset) {
|
|
|
1227
1412
|
* result instead of parsing manifest fields directly.
|
|
1228
1413
|
*/
|
|
1229
1414
|
function planLoad(inputPath, opts = {}) {
|
|
1230
|
-
let
|
|
1415
|
+
let layout;
|
|
1416
|
+
|
|
1417
|
+
if (Buffer.isBuffer(inputPath) || inputPath instanceof Uint8Array) {
|
|
1418
|
+
try {
|
|
1419
|
+
layout = readInputLayout(inputPath);
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
return finalizeLoadPlan({
|
|
1422
|
+
format_version: null,
|
|
1423
|
+
asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
|
|
1424
|
+
access: null, access_alias: null, entitlement_profile: null,
|
|
1425
|
+
state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
|
|
1426
|
+
input_fingerprint: null,
|
|
1427
|
+
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1428
|
+
issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
|
|
1429
|
+
source: inputSource(inputPath, 'memory'),
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1231
1433
|
|
|
1232
1434
|
// Try the unified container dispatcher first; the internal compatibility
|
|
1233
1435
|
// reader below remains available only for authoring and migration tooling.
|
|
1234
|
-
if (readAsset !== null) {
|
|
1436
|
+
if (!layout && readAsset !== null) {
|
|
1235
1437
|
try {
|
|
1236
1438
|
const asset = readAsset(path.resolve(inputPath));
|
|
1237
|
-
|
|
1439
|
+
layout = canonicalToContainerLayout(asset);
|
|
1238
1440
|
} catch (e) {
|
|
1239
|
-
// If dispatcher fails, fall back to legacy
|
|
1441
|
+
// If dispatcher fails, fall back to legacy readLayout for backward compat
|
|
1240
1442
|
if (e.code === 'KDNA_FORMAT_UNKNOWN' || e.code === 'KDNA_PATH_NOT_FOUND') {
|
|
1241
1443
|
// let the legacy path handle it
|
|
1242
1444
|
} else {
|
|
1243
1445
|
return finalizeLoadPlan({
|
|
1244
|
-
|
|
1446
|
+
format_version: null,
|
|
1245
1447
|
asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
|
|
1246
1448
|
access: null, access_alias: null, entitlement_profile: null,
|
|
1247
1449
|
state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
|
|
@@ -1251,33 +1453,37 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1251
1453
|
e.code === 'KDNA_FORMAT_UNKNOWN' ? 'KDNA_FORMAT_UNKNOWN' : 'KDNA_FORMAT_INVALID',
|
|
1252
1454
|
'blocking', e.message,
|
|
1253
1455
|
)],
|
|
1254
|
-
source:
|
|
1456
|
+
source: inputSource(inputPath, null),
|
|
1255
1457
|
});
|
|
1256
1458
|
}
|
|
1257
1459
|
}
|
|
1258
1460
|
}
|
|
1259
1461
|
|
|
1260
|
-
if (!
|
|
1462
|
+
if (!layout) {
|
|
1261
1463
|
try {
|
|
1262
|
-
|
|
1464
|
+
layout = readInputLayout(inputPath);
|
|
1263
1465
|
} catch (e) {
|
|
1264
1466
|
return finalizeLoadPlan({
|
|
1265
|
-
|
|
1467
|
+
format_version: null,
|
|
1266
1468
|
asset: { asset_id: null, asset_uid: null, title: null, version: null, judgment_version: null },
|
|
1267
1469
|
access: null, access_alias: null, entitlement_profile: null,
|
|
1268
1470
|
state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
|
|
1269
1471
|
input_fingerprint: null,
|
|
1270
1472
|
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1271
1473
|
issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
|
|
1272
|
-
source:
|
|
1474
|
+
source: inputSource(inputPath, null),
|
|
1273
1475
|
});
|
|
1274
1476
|
}
|
|
1275
1477
|
}
|
|
1276
1478
|
|
|
1277
|
-
const validation = runValidate(
|
|
1278
|
-
const plan = baseLoadPlan(inputPath,
|
|
1479
|
+
const validation = runValidate(layout);
|
|
1480
|
+
const plan = baseLoadPlan(inputPath, layout, validation, opts);
|
|
1279
1481
|
|
|
1280
1482
|
if (!validation.overall_valid) {
|
|
1483
|
+
if (!['public', 'licensed', 'remote'].includes(plan.access)) {
|
|
1484
|
+
plan.access = null;
|
|
1485
|
+
plan.access_alias = null;
|
|
1486
|
+
}
|
|
1281
1487
|
plan.state = 'invalid';
|
|
1282
1488
|
plan.required_action = 'block';
|
|
1283
1489
|
plan.can_load_now = false;
|
|
@@ -1288,22 +1494,27 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1288
1494
|
return finalizeLoadPlan(plan);
|
|
1289
1495
|
}
|
|
1290
1496
|
|
|
1291
|
-
const 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
|
+
}
|
|
1292
1511
|
|
|
1293
1512
|
function externalEntitlementMatchesCurrentAsset(entitlement) {
|
|
1294
1513
|
if (!isVerifiedExternalEntitlement(entitlement) || !entitlement.asset) return false;
|
|
1295
|
-
|
|
1296
|
-
try {
|
|
1297
|
-
checksums = v1.map['checksums.json']
|
|
1298
|
-
? parseJsonEntry('checksums.json', v1.map['checksums.json'])
|
|
1299
|
-
: null;
|
|
1300
|
-
} catch {
|
|
1301
|
-
return false;
|
|
1302
|
-
}
|
|
1303
|
-
return entitlement.asset.asset_id === (manifest.asset_id || manifest.name)
|
|
1514
|
+
return entitlement.asset.asset_id === manifest.asset_id
|
|
1304
1515
|
&& entitlement.asset.asset_uid === manifest.asset_uid
|
|
1305
1516
|
&& entitlement.asset.version === manifest.version
|
|
1306
|
-
&& entitlement.asset.digest ===
|
|
1517
|
+
&& entitlement.asset.digest === layout.containerDigest
|
|
1307
1518
|
&& entitlement.asset.entry_path === manifest.payload?.path;
|
|
1308
1519
|
}
|
|
1309
1520
|
|
|
@@ -1698,34 +1909,46 @@ function assertNoForbiddenTerms(obj) {
|
|
|
1698
1909
|
walk(obj);
|
|
1699
1910
|
if (seen.size > 0) {
|
|
1700
1911
|
throw new Error(
|
|
1701
|
-
`internal:
|
|
1912
|
+
`internal: layout inspect output contains forbidden terms: ${[...seen].join(', ')}`,
|
|
1702
1913
|
);
|
|
1703
1914
|
}
|
|
1704
1915
|
}
|
|
1705
1916
|
|
|
1706
1917
|
module.exports = {
|
|
1707
1918
|
MIMETYPE,
|
|
1919
|
+
KDNA_LOADER_VERSION,
|
|
1708
1920
|
REQUIRED_DIR_ENTRIES,
|
|
1709
1921
|
isKdnaSourceDir,
|
|
1710
1922
|
detectContainerFormat,
|
|
1711
|
-
readLayout:
|
|
1923
|
+
readLayout: readLayout,
|
|
1712
1924
|
inspect,
|
|
1713
1925
|
validate,
|
|
1714
1926
|
planLoad,
|
|
1715
1927
|
loadAuthorized,
|
|
1716
|
-
buildChecksums:
|
|
1928
|
+
buildChecksums: buildChecksums,
|
|
1929
|
+
computeRuntimeEntrySetDigest,
|
|
1717
1930
|
pack,
|
|
1718
1931
|
unpack,
|
|
1719
|
-
load:
|
|
1720
|
-
loadAsset:
|
|
1721
|
-
buildCapsule,
|
|
1932
|
+
load: loadAsset,
|
|
1933
|
+
loadAsset: loadAsset,
|
|
1722
1934
|
FORBIDDEN_OUTPUT_TERMS,
|
|
1723
1935
|
parseSemver,
|
|
1724
1936
|
compareSemver,
|
|
1725
1937
|
satisfies,
|
|
1726
1938
|
};
|
|
1727
1939
|
|
|
1728
|
-
//
|
|
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 ──────────────────────
|
|
1729
1952
|
|
|
1730
1953
|
function renderPromptItem(item) {
|
|
1731
1954
|
if (item === undefined || item === null) return '';
|
|
@@ -1746,7 +1969,7 @@ function renderPromptItem(item) {
|
|
|
1746
1969
|
// Boundary card: kdna-studio writes { id, scope, out_of_scope,
|
|
1747
1970
|
// acceptable_exceptions }. Render the scope/out-of-scope rule
|
|
1748
1971
|
// instead of falling through to the UUID `id` (which was the
|
|
1749
|
-
//
|
|
1972
|
+
// 0.15.0 behavior and made boundaries unreadable in
|
|
1750
1973
|
// `kdna load --as=prompt`).
|
|
1751
1974
|
if (
|
|
1752
1975
|
item.type === 'boundary' ||
|
|
@@ -1806,7 +2029,12 @@ function sanitizePrompt(s) {
|
|
|
1806
2029
|
}
|
|
1807
2030
|
|
|
1808
2031
|
function loadAuthorized(inputPath, opts = {}) {
|
|
1809
|
-
|
|
2032
|
+
// A packaged path is read exactly once. Authorization and projection must
|
|
2033
|
+
// operate on the same immutable byte snapshot even if the path changes
|
|
2034
|
+
// while this synchronous call is in progress.
|
|
2035
|
+
const inputKind = typeof inputPath === 'string' ? 'packaged_file' : 'packaged_bytes';
|
|
2036
|
+
const inputSnapshot = snapshotPackagedInput(inputPath);
|
|
2037
|
+
const plan = planLoad(inputSnapshot, opts);
|
|
1810
2038
|
if (plan.can_load_now !== true) {
|
|
1811
2039
|
const issueCodes = Array.isArray(plan.issues)
|
|
1812
2040
|
? plan.issues.map((issue) => issue.code).filter(Boolean)
|
|
@@ -1826,11 +2054,155 @@ function loadAuthorized(inputPath, opts = {}) {
|
|
|
1826
2054
|
schema_valid: plan.checks && plan.checks.overall_valid === true,
|
|
1827
2055
|
signature_valid: plan.signature_valid === true,
|
|
1828
2056
|
},
|
|
2057
|
+
_runtimeAssetBytes: inputSnapshot,
|
|
2058
|
+
_runtimeInputKind: inputKind,
|
|
1829
2059
|
};
|
|
1830
|
-
return
|
|
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
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
|
|
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
|
+
});
|
|
1831
2110
|
}
|
|
1832
2111
|
|
|
1833
|
-
function
|
|
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 = {}) {
|
|
1834
2206
|
return loadAuthorized(inputPath, opts);
|
|
1835
2207
|
}
|
|
1836
2208
|
|
|
@@ -1894,15 +2266,15 @@ function hasCompactPromptContent(content) {
|
|
|
1894
2266
|
);
|
|
1895
2267
|
}
|
|
1896
2268
|
|
|
1897
|
-
function
|
|
1898
|
-
const
|
|
1899
|
-
const m =
|
|
2269
|
+
function loadAssetUnsafe(inputPath, opts = {}) {
|
|
2270
|
+
const layout = readInputLayout(inputPath);
|
|
2271
|
+
const m = layout.manifest;
|
|
1900
2272
|
const profile = opts.profile || (m.load_contract ? m.load_contract.default_profile : 'compact') || 'compact';
|
|
1901
2273
|
const as = opts.as || 'json';
|
|
1902
2274
|
|
|
1903
2275
|
let payload;
|
|
1904
2276
|
try {
|
|
1905
|
-
const rawPayload =
|
|
2277
|
+
const rawPayload = layout.map['payload.kdnab'];
|
|
1906
2278
|
payload = parsePayloadEntry("payload.kdnab", rawPayload);
|
|
1907
2279
|
} catch (e) {
|
|
1908
2280
|
throw new Error(`payload.kdnab is not valid KDNA payload: ${e.message}`, { cause: e });
|
|
@@ -1914,10 +2286,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1914
2286
|
// Consumer-provided decrypt hook (e.g., licensed entry key)
|
|
1915
2287
|
try {
|
|
1916
2288
|
const decrypted = opts.decryptEntry({
|
|
1917
|
-
asset: { entries:
|
|
2289
|
+
asset: { entries: layout.map, manifest: m },
|
|
1918
2290
|
manifest: m,
|
|
1919
2291
|
entryName: 'payload.kdnab',
|
|
1920
|
-
ciphertext:
|
|
2292
|
+
ciphertext: layout.map['payload.kdnab'],
|
|
1921
2293
|
});
|
|
1922
2294
|
const decryptedBuf = typeof decrypted === 'string'
|
|
1923
2295
|
? Buffer.from(decrypted, 'utf8')
|
|
@@ -1929,7 +2301,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1929
2301
|
throw err;
|
|
1930
2302
|
}
|
|
1931
2303
|
} else if (opts.password) {
|
|
1932
|
-
// Password-based decryption via kdna
|
|
2304
|
+
// Password-based decryption via kdna.encryption.password*
|
|
1933
2305
|
// profiles. Detects Argon2id vs scrypt from the envelope profile.
|
|
1934
2306
|
try {
|
|
1935
2307
|
const {
|
|
@@ -1937,10 +2309,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1937
2309
|
decryptProtectedEntryScrypt,
|
|
1938
2310
|
PASSWORD_PROTECTED_SCRYPT_PROFILE,
|
|
1939
2311
|
} = require('../crypto-profile.js');
|
|
1940
|
-
const encryptedEnvelope =
|
|
2312
|
+
const encryptedEnvelope = layout.map['payload.kdnab'];
|
|
1941
2313
|
let envelope;
|
|
1942
2314
|
try { envelope = cbor.decode(Buffer.isBuffer(encryptedEnvelope) ? encryptedEnvelope : Buffer.from(encryptedEnvelope)); }
|
|
1943
|
-
catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (
|
|
2315
|
+
catch { throw new Error('Cannot decode encrypted envelope: payload.kdnab must be CBOR-encoded (format 0.1.0)'); }
|
|
1944
2316
|
const decryptFn = (envelope.profile === PASSWORD_PROTECTED_SCRYPT_PROFILE)
|
|
1945
2317
|
? decryptProtectedEntryScrypt
|
|
1946
2318
|
: decryptProtectedEntry;
|
|
@@ -1968,11 +2340,11 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
1968
2340
|
}
|
|
1969
2341
|
|
|
1970
2342
|
// Digest verification — refuse to load if checksums.json is present and digests mismatch.
|
|
1971
|
-
if (
|
|
2343
|
+
if (layout.map['checksums.json']) {
|
|
1972
2344
|
try {
|
|
1973
|
-
const checks = parseJsonEntry('checksums.json',
|
|
2345
|
+
const checks = parseJsonEntry('checksums.json', layout.map['checksums.json']);
|
|
1974
2346
|
const problems = [];
|
|
1975
|
-
const ok = verifyDigests(checks,
|
|
2347
|
+
const ok = verifyDigests(checks, layout.map, problems, {});
|
|
1976
2348
|
if (!ok) {
|
|
1977
2349
|
const err = new Error(`checksum verification failed: ${problems.join('; ')}`);
|
|
1978
2350
|
err.code = 'checksum_mismatch';
|
|
@@ -2020,7 +2392,8 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2020
2392
|
if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.compact && m.load_contract.profiles.compact.max_tokens_hint) {
|
|
2021
2393
|
maxTokensHint = m.load_contract.profiles.compact.max_tokens_hint;
|
|
2022
2394
|
}
|
|
2023
|
-
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 || {}) : []
|
|
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;
|
|
2024
2397
|
} else if (profile === 'compact') {
|
|
2025
2398
|
const core = payload.core || {};
|
|
2026
2399
|
const normalizeList = (items) => (items || []).map((item) => {
|
|
@@ -2028,6 +2401,13 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2028
2401
|
if (item && typeof item === 'object') return item;
|
|
2029
2402
|
return null;
|
|
2030
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
|
+
);
|
|
2031
2411
|
result.content = {
|
|
2032
2412
|
highest_question: core.highest_question || null,
|
|
2033
2413
|
// These scoped domain-level values are already validated by the payload
|
|
@@ -2043,11 +2423,10 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2043
2423
|
: null,
|
|
2044
2424
|
axioms: (core.axioms || []).map(normalizeCompactAxiom).filter(Boolean),
|
|
2045
2425
|
boundaries: normalizeList(core.boundaries),
|
|
2046
|
-
//
|
|
2047
|
-
//
|
|
2048
|
-
//
|
|
2049
|
-
|
|
2050
|
-
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),
|
|
2051
2430
|
failure_modes: normalizeList(payload.reasoning && payload.reasoning.failure_modes),
|
|
2052
2431
|
patterns: normalizeList(payload.patterns).slice(0, 3),
|
|
2053
2432
|
};
|
|
@@ -2069,7 +2448,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2069
2448
|
if (opts.extendsChain && opts.extendsChain.length > 0 && result.content) {
|
|
2070
2449
|
try {
|
|
2071
2450
|
const base = opts.extendsChain[0];
|
|
2072
|
-
const baseLoaded =
|
|
2451
|
+
const baseLoaded = loadAssetUnsafe(base.path, {
|
|
2073
2452
|
...opts,
|
|
2074
2453
|
resolvedDependencies: [],
|
|
2075
2454
|
extendsChain: [],
|
|
@@ -2113,7 +2492,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2113
2492
|
|
|
2114
2493
|
if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
|
|
2115
2494
|
result.resolved_dependencies = opts.resolvedDependencies.map(dep => {
|
|
2116
|
-
const depLoaded =
|
|
2495
|
+
const depLoaded = loadAssetUnsafe(dep.path, { ...opts, resolvedDependencies: [] });
|
|
2117
2496
|
// RAG namespace (Story 11): scoped identifier for namespace isolation.
|
|
2118
2497
|
// Format: name@version (or just name when version is absent).
|
|
2119
2498
|
const ragNamespace = dep.name
|
|
@@ -2199,7 +2578,7 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2199
2578
|
let textOut = text.trim();
|
|
2200
2579
|
if (opts.resolvedDependencies && opts.resolvedDependencies.length > 0) {
|
|
2201
2580
|
for (const dep of opts.resolvedDependencies) {
|
|
2202
|
-
const depLoaded =
|
|
2581
|
+
const depLoaded = loadAssetUnsafe(dep.path, { ...opts, resolvedDependencies: [] });
|
|
2203
2582
|
if (depLoaded.text) {
|
|
2204
2583
|
// Namespace header (Story 11): each component section is prefixed
|
|
2205
2584
|
// with [NAMESPACE: id] so consumers can isolate RAG content per source.
|
|
@@ -2218,59 +2597,37 @@ function loadV1Unsafe(inputPath, opts = {}) {
|
|
|
2218
2597
|
return result;
|
|
2219
2598
|
}
|
|
2220
2599
|
|
|
2221
|
-
return wrapAsCapsule(result,
|
|
2600
|
+
return wrapAsCapsule(result, layout, profile, { ...opts, as });
|
|
2222
2601
|
}
|
|
2223
2602
|
|
|
2224
|
-
function wrapAsCapsule(result,
|
|
2603
|
+
function wrapAsCapsule(result, layout, profile, opts) {
|
|
2225
2604
|
if (opts.as === 'json') {
|
|
2226
|
-
return
|
|
2605
|
+
return buildRuntimeCapsuleProjection(result, layout, profile, opts);
|
|
2227
2606
|
}
|
|
2228
2607
|
return result;
|
|
2229
2608
|
}
|
|
2230
2609
|
|
|
2231
|
-
function
|
|
2232
|
-
const m =
|
|
2610
|
+
function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
|
|
2611
|
+
const m = layout.manifest;
|
|
2233
2612
|
const val = opts._validation || {};
|
|
2234
|
-
const
|
|
2235
|
-
? (() => {
|
|
2236
|
-
try {
|
|
2237
|
-
const checks = JSON.parse(v1.map['checksums.json'].toString('utf8'));
|
|
2238
|
-
return checks.asset_digest || null;
|
|
2239
|
-
} catch { return null; }
|
|
2240
|
-
})()
|
|
2241
|
-
: null);
|
|
2242
|
-
|
|
2243
|
-
const pubkey = (m.creator && m.creator.pubkey) || (m.author && m.author.pubkey) || null;
|
|
2613
|
+
const pubkey = (m.creator && m.creator.pubkey) || null;
|
|
2244
2614
|
const sigVerified = val.signature_valid === true;
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
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,
|
|
2251
2623
|
signature: pubkey
|
|
2252
2624
|
? { state: sigVerified ? 'verified' : 'not_checked', issuer: pubkey }
|
|
2253
2625
|
: { state: 'absent' },
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
payload_encoding: (m.payload && m.payload.encoding) || 'cbor',
|
|
2260
|
-
loaded_by: 'kdna-core',
|
|
2261
|
-
loaded_at: new Date().toISOString(),
|
|
2262
|
-
schema_valid: val.schema_valid === true,
|
|
2263
|
-
signature_state: sigVerified ? 'verified' : (pubkey ? 'not_checked' : 'absent'),
|
|
2264
|
-
profile: profile,
|
|
2265
|
-
},
|
|
2266
|
-
};
|
|
2267
|
-
|
|
2268
|
-
if (loadResult.extends_chain) capsule.extends_chain = loadResult.extends_chain;
|
|
2269
|
-
if (loadResult.inheritance_applied !== undefined) capsule.inheritance_applied = loadResult.inheritance_applied;
|
|
2270
|
-
if (loadResult.resolved_dependencies) capsule.resolved_dependencies = loadResult.resolved_dependencies;
|
|
2271
|
-
if (loadResult.rag_isolation_policy) capsule.rag_isolation_policy = loadResult.rag_isolation_policy;
|
|
2272
|
-
|
|
2273
|
-
return capsule;
|
|
2626
|
+
inputKind: opts._runtimeInputKind,
|
|
2627
|
+
loadedAt: opts.loadedAt,
|
|
2628
|
+
schemaValid: val.schema_valid === true,
|
|
2629
|
+
profile,
|
|
2630
|
+
});
|
|
2274
2631
|
}
|
|
2275
2632
|
|
|
2276
2633
|
// ─── Semver & Dependency Resolution (Story 6) ─────────────────────────
|
|
@@ -2286,9 +2643,9 @@ function parseSemver(v) {
|
|
|
2286
2643
|
};
|
|
2287
2644
|
}
|
|
2288
2645
|
|
|
2289
|
-
function compareSemver(
|
|
2290
|
-
const p1 = parseSemver(
|
|
2291
|
-
const p2 = parseSemver(
|
|
2646
|
+
function compareSemver(leftVersion, rightVersion) {
|
|
2647
|
+
const p1 = parseSemver(leftVersion);
|
|
2648
|
+
const p2 = parseSemver(rightVersion);
|
|
2292
2649
|
if (!p1 || !p2) return 0;
|
|
2293
2650
|
if (p1.major !== p2.major) return p1.major - p2.major;
|
|
2294
2651
|
if (p1.minor !== p2.minor) return p1.minor - p2.minor;
|