@aikdna/kdna-core 0.21.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 CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.22.0 (2026-08-17)
6
+
7
+ ### Added
8
+
9
+ - RFC-0021 M1 asset signatures (`kdsig.ed25519`): sign and verify packaged
10
+ `.kdna` assets with Ed25519 signature bundles — `signKDNA`,
11
+ `verifyKDNASignature`, `composeKDNA`, signing-payload builders, key-pair
12
+ generation, key fingerprints, and signature-bundle parse/serialize —
13
+ mirrored byte-for-byte in the Python Core, with cross-language signature
14
+ conformance vectors. M1 is bound as a pre-release candidate; M2–M6 are not
15
+ implemented and remain non-binding.
16
+ - RFC-0020 minimal projection profile: a fifth load profile for small local
17
+ models, a strict subset of compact carrying the highest-question and
18
+ boundary-friendly axioms plus full boundaries (~0.8–1.1k tokens vs compact
19
+ ~5.4k on the reference assets). Availability follows the manifest
20
+ declaration; assets without a minimal declaration fail closed.
21
+
22
+ ### Fixed
23
+
5
24
  - Validate decrypted external-grant and password payload plaintext against the
6
25
  same declared Payload schema used for ordinary assets. Correct credentials
7
26
  can no longer load schema-invalid judgment bytes.
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # @aikdna/kdna-core
2
2
 
3
- > **Status:** Pre-release. This source tree contains the unreleased `0.21.0`
4
- > corrective candidate; the published incumbent is `0.20.0`. Candidate source,
5
- > APIs, and evidence must not be described as already published.
3
+ > **Status:** Pre-release. This source tree contains unreleased work beyond
4
+ > the published incumbent `0.22.0` (RFC-0021 M1 asset signatures and the
5
+ > RFC-0020 minimal projection profile); the next release coordinate is not
6
+ > finalized. The older `0.21.0` line predates the signature track. Candidate
7
+ > source, APIs, and evidence must not be described as already published.
6
8
 
7
9
  Core library for packaged `.kdna` judgment assets. It implements one current
8
10
  container contract, authorization planning, integrity evidence, Runtime Capsule
@@ -11,11 +13,11 @@ projection, and the correlated Agent Host execution boundary.
11
13
  ## Install
12
14
 
13
15
  ```bash
14
- npm install @aikdna/kdna-core@0.20.0
16
+ npm install @aikdna/kdna-core@0.22.0
15
17
  ```
16
18
 
17
19
  That command installs the published incumbent. Use its release notes for the
18
- exact `0.20.0` contract. The examples below describe the current source
20
+ exact `0.22.0` contract. The examples below describe the current source
19
21
  candidate and require its final release coordinate before registry use.
20
22
 
21
23
  ## Pack, validate, plan, and load
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/kdna-core",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "KDNA Core library for validating, planning, and loading local .kdna judgment assets.",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@noble/hashes": "^1.8.0",
67
- "cbor-x": "^1.6.4",
67
+ "cbor-x": "^1.6.5",
68
68
  "ajv": "^8.20.0",
69
69
  "ajv-formats": "^3.0.1"
70
70
  }
@@ -9,7 +9,7 @@
9
9
  "properties": {
10
10
  "default_profile": {
11
11
  "type": "string",
12
- "enum": ["index", "compact", "scenario", "full"]
12
+ "enum": ["index", "compact", "scenario", "full", "minimal"]
13
13
  },
14
14
  "profiles": {
15
15
  "type": "object",
@@ -19,7 +19,8 @@
19
19
  "index": { "$ref": "#/$defs/profile" },
20
20
  "compact": { "$ref": "#/$defs/profile" },
21
21
  "scenario": { "$ref": "#/$defs/profile" },
22
- "full": { "$ref": "#/$defs/profile" }
22
+ "full": { "$ref": "#/$defs/profile" },
23
+ "minimal": { "$ref": "#/$defs/profile" }
23
24
  }
24
25
  }
25
26
  },
@@ -15,6 +15,7 @@
15
15
  "can_load_now",
16
16
  "projection_policy",
17
17
  "checks",
18
+ "signature_state",
18
19
  "issues",
19
20
  "source",
20
21
  "input_fingerprint"
@@ -87,6 +88,7 @@
87
88
  "schema_valid",
88
89
  "payload_valid",
89
90
  "checksums_valid",
91
+ "signature_valid",
90
92
  "load_contract_valid",
91
93
  "overall_valid"
92
94
  ],
@@ -96,10 +98,15 @@
96
98
  "schema_valid": { "type": "boolean" },
97
99
  "payload_valid": { "type": "boolean" },
98
100
  "checksums_valid": { "type": "boolean" },
101
+ "signature_valid": { "type": "boolean" },
99
102
  "load_contract_valid": { "type": "boolean" },
100
103
  "overall_valid": { "type": "boolean" }
101
104
  }
102
105
  },
106
+ "signature_state": {
107
+ "description": "RFC-0021 M1 signature evidence state: absent (unsigned), verified (signature.kdsig verified), or invalid (present but rejected).",
108
+ "enum": ["absent", "verified", "invalid"]
109
+ },
103
110
  "issues": {
104
111
  "type": "array",
105
112
  "items": {
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "signature": { "$ref": "#/$defs/signature" },
38
38
  "access": { "enum": ["public", "licensed", "remote"] },
39
- "profile": { "enum": ["index", "compact", "scenario", "full"] },
39
+ "profile": { "enum": ["index", "compact", "scenario", "full", "minimal"] },
40
40
  "context": { "type": "object" },
41
41
  "trace": { "$ref": "#/$defs/trace" }
42
42
  },
@@ -78,12 +78,28 @@
78
78
  }
79
79
  },
80
80
  "signature": {
81
- "type": "object",
82
- "required": ["state"],
83
- "additionalProperties": false,
84
- "properties": {
85
- "state": { "const": "absent" }
86
- }
81
+ "oneOf": [
82
+ {
83
+ "type": "object",
84
+ "required": ["state"],
85
+ "additionalProperties": false,
86
+ "properties": {
87
+ "state": { "const": "absent" }
88
+ }
89
+ },
90
+ {
91
+ "type": "object",
92
+ "required": ["state", "profile", "profile_version", "key_fingerprint", "content_digest"],
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "state": { "const": "verified" },
96
+ "profile": { "const": "kdsig.ed25519" },
97
+ "profile_version": { "const": "0.1.0" },
98
+ "key_fingerprint": { "$ref": "#/$defs/sha256" },
99
+ "content_digest": { "$ref": "#/$defs/sha256" }
100
+ }
101
+ }
102
+ ]
87
103
  },
88
104
  "trace": {
89
105
  "type": "object",
@@ -105,8 +121,8 @@
105
121
  "input_kind": { "enum": ["packaged_file", "packaged_bytes"] },
106
122
  "runtime_eligible": { "const": true },
107
123
  "schema_valid": { "const": true },
108
- "signature_state": { "const": "absent" },
109
- "profile": { "enum": ["index", "compact", "scenario", "full"] },
124
+ "signature_state": { "enum": ["absent", "verified"] },
125
+ "profile": { "enum": ["index", "compact", "scenario", "full", "minimal"] },
110
126
  "projection_report": { "$ref": "#/$defs/projectionReport" }
111
127
  }
112
128
  },
@@ -7,6 +7,7 @@ const crypto = require('crypto');
7
7
  const zlib = require('zlib');
8
8
  const cbor = require('cbor-x');
9
9
  const { loaderVersionUnsupportedMessage } = require('./loader-compatibility');
10
+ const { SIGNATURE_ENTRY_NAME } = require('./signature');
10
11
 
11
12
  const KDNA_MEDIA_TYPE = 'application/vnd.kdna.asset';
12
13
  const ASSET_SOURCE_BYTES = Symbol.for('@aikdna/kdna-core.asset-source-bytes');
@@ -46,10 +47,6 @@ function compareEntryNamesByUtf8(left, right) {
46
47
  return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
47
48
  }
48
49
 
49
- function sortedEntryNames(entries) {
50
- return [...entries.keys()].sort(compareEntryNamesByUtf8);
51
- }
52
-
53
50
  function parseJson(buf, entryName) {
54
51
  try {
55
52
  return JSON.parse(Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf));
@@ -202,13 +199,23 @@ function manifestForDigest(manifest) {
202
199
  return copy;
203
200
  }
204
201
 
205
- function buildContentDigest(asset) {
202
+ // Entries excluded from the canonical content tree (docs/CANONICALIZATION.md).
203
+ // signature.kdsig can never be part of the content it signs.
204
+ const CONTENT_DIGEST_EXCLUDED_ENTRIES = new Set(['.DS_Store', 'build-receipt.json', 'signature.kdsig']);
205
+
206
+ /**
207
+ * Compute the canonical content digest over a map of entry name → Buffer.
208
+ * Shared by the asset reader and the container signature gate so both always
209
+ * hash the same canonical bytes.
210
+ */
211
+ function contentDigestFromEntryBuffers(buffers) {
212
+ const names = buffers instanceof Map ? [...buffers.keys()] : Object.keys(buffers);
213
+ const readBuffer = (name) => (buffers instanceof Map ? buffers.get(name) : buffers[name]);
206
214
  const parts = [];
207
- const excluded = new Set(['.DS_Store', 'build-receipt.json']);
208
- for (const entryName of sortedEntryNames(asset.entries)) {
209
- if (excluded.has(entryName)) continue;
215
+ for (const entryName of names.sort(compareEntryNamesByUtf8)) {
216
+ if (CONTENT_DIGEST_EXCLUDED_ENTRIES.has(entryName)) continue;
210
217
  if (entryName.startsWith('reports/')) continue;
211
- const entryBuf = asset.readEntry(entryName);
218
+ const entryBuf = readBuffer(entryName);
212
219
  let digestBuf = entryBuf;
213
220
  if (JSON_ENTRY_RE.test(entryName)) {
214
221
  const parsed = parseJson(entryBuf, entryName);
@@ -220,6 +227,14 @@ function buildContentDigest(asset) {
220
227
  return `sha256:${sha256Hex(Buffer.from(parts.join('\n')))}`;
221
228
  }
222
229
 
230
+ function buildContentDigest(asset) {
231
+ const buffers = new Map();
232
+ for (const entryName of asset.entries.keys()) {
233
+ buffers.set(entryName, asset.readEntry(entryName));
234
+ }
235
+ return contentDigestFromEntryBuffers(buffers);
236
+ }
237
+
223
238
  function verifyMediaType(asset, errors) {
224
239
  if (!asset.entries.has('mimetype')) {
225
240
  errors.push('required entry missing: mimetype');
@@ -380,8 +395,8 @@ function verifySync(asset, options = {}) {
380
395
  // adds transport identity; it must not maintain a second manifest or payload
381
396
  // dialect.
382
397
  appendCurrentValidationErrors(asset, errors);
383
- if (options.requireSignature === true) {
384
- errors.push('asset signatures are outside the current Preview contract');
398
+ if (options.requireSignature === true && !asset.entries.has(SIGNATURE_ENTRY_NAME)) {
399
+ errors.push(`required asset signature is absent: missing entry ${SIGNATURE_ENTRY_NAME}`);
385
400
  }
386
401
 
387
402
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
@@ -541,8 +556,8 @@ function createKdnaAssetReader() {
541
556
  const entries = [...asset.entries.keys()].sort();
542
557
 
543
558
  appendCurrentValidationErrors(asset, errors);
544
- if (options.requireSignature === true) {
545
- errors.push('asset signatures are outside the current Preview contract');
559
+ if (options.requireSignature === true && !asset.entries.has(SIGNATURE_ENTRY_NAME)) {
560
+ errors.push(`required asset signature is absent: missing entry ${SIGNATURE_ENTRY_NAME}`);
546
561
  }
547
562
 
548
563
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
@@ -617,5 +632,6 @@ function createKdnaAssetReader() {
617
632
 
618
633
  module.exports = {
619
634
  STANDARD_ENTRIES,
635
+ contentDigestFromEntryBuffers,
620
636
  createKdnaAssetReader,
621
637
  };