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