@aikdna/kdna-cli 0.19.3 → 0.20.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.
@@ -0,0 +1,117 @@
1
+ // KDNA dev pack — v2 container support
2
+ // Produces KDNA Container v2 (.kdna files with CBOR payload.kdnab) from dev source directories.
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+
7
+ let _cbor = null;
8
+ function getCbor() {
9
+ if (!_cbor) {
10
+ try {
11
+ _cbor = require('cbor-x');
12
+ } catch {
13
+ try {
14
+ _cbor = require('@aikdna/kdna-core/../cbor-x');
15
+ } catch {
16
+ throw new Error('cbor-x is required for dev pack v2. Install: npm install cbor-x');
17
+ }
18
+ }
19
+ }
20
+ return _cbor;
21
+ }
22
+
23
+ const KDNA_FILES = [
24
+ 'KDNA_Core.json',
25
+ 'KDNA_Patterns.json',
26
+ 'KDNA_Scenarios.json',
27
+ 'KDNA_Cases.json',
28
+ 'KDNA_Reasoning.json',
29
+ 'KDNA_Evolution.json',
30
+ ];
31
+
32
+ function packV2(sourceDir, manifest, options = {}) {
33
+ const abs = path.resolve(sourceDir);
34
+
35
+ // 1. Read source files
36
+ const judgment = {};
37
+ for (const f of KDNA_FILES) {
38
+ const filePath = path.join(abs, f);
39
+ if (fs.existsSync(filePath)) {
40
+ try {
41
+ judgment[
42
+ f
43
+ .replace(/^KDNA_/, '')
44
+ .replace(/\.json$/, '')
45
+ .toLowerCase()
46
+ ] = JSON.parse(fs.readFileSync(filePath, 'utf8'));
47
+ } catch {
48
+ /* skip unreadable */
49
+ }
50
+ }
51
+ }
52
+
53
+ // 2. Build CBOR payload
54
+ const payload = {
55
+ kind: 'kdna.payload',
56
+ payload_version: '2.0',
57
+ domain: { name: manifest.name || '', version: manifest.version || '0.1.0' },
58
+ judgment,
59
+ profiles: {},
60
+ integrity: {
61
+ source_tree_digest: `sha256:${computeDirHash(abs)}`,
62
+ },
63
+ };
64
+ const payloadBuf = getCbor().encode(payload);
65
+
66
+ // 3. Build manifest (v2)
67
+ const v2Manifest = {
68
+ ...manifest,
69
+ format_version: '2.0',
70
+ spec_version: '2.0',
71
+ container: {
72
+ type: 'kdna-container-v2',
73
+ payload: 'payload.kdnab',
74
+ payload_encoding: 'cbor',
75
+ payload_schema: 'kdna-payload-v2',
76
+ payload_digest: `sha256:${crypto.createHash('sha256').update(payloadBuf).digest('hex')}`,
77
+ },
78
+ runtime: {
79
+ min_runtime_version: '0.3.0',
80
+ load_contract: 'context-capsule-v1',
81
+ },
82
+ };
83
+
84
+ return {
85
+ entries: {
86
+ mimetype: 'application/vnd.aikdna.kdna+zip',
87
+ 'kdna.json': JSON.stringify(v2Manifest, null, 2),
88
+ 'payload.kdnab': payloadBuf,
89
+ },
90
+ manifest: v2Manifest,
91
+ payload,
92
+ };
93
+ }
94
+
95
+ function computeDirHash(dir) {
96
+ const files = fs
97
+ .readdirSync(dir)
98
+ .filter((f) => fs.statSync(path.join(dir, f)).isFile())
99
+ .sort();
100
+ const hash = crypto.createHash('sha256');
101
+ for (const f of files) {
102
+ hash.update(f);
103
+ hash.update(fs.readFileSync(path.join(dir, f)));
104
+ }
105
+ return hash.digest('hex');
106
+ }
107
+
108
+ function verifySourceIntegrity(sourceDir, expectedDigest) {
109
+ if (!expectedDigest) return { valid: false, error: 'No expected digest provided' };
110
+ const actual = `sha256:${computeDirHash(sourceDir)}`;
111
+ if (actual !== expectedDigest) {
112
+ return { valid: false, error: `Digest mismatch: expected ${expectedDigest}, got ${actual}` };
113
+ }
114
+ return { valid: true, digest: actual };
115
+ }
116
+
117
+ module.exports = { packV2, verifySourceIntegrity, computeDirHash };
package/src/init.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * kdna init <name> — Scaffold a new KDNA domain from template.
2
+ * kdna init <name> — Deprecated alias for kdna dev scaffold <name>.
3
+ * kdna dev scaffold <name> — Scaffold a non-canonical dev source workspace.
3
4
  * kdna cluster init <name> — Scaffold a new KDNA cluster from template.
4
5
  */
5
6
 
@@ -27,9 +28,9 @@ function copyRecursive(src, dest, replacements) {
27
28
  }
28
29
  }
29
30
 
30
- function cmdInit(name) {
31
+ function cmdInit(name, options = {}) {
31
32
  if (!name) {
32
- console.error('Error: Domain name required. Usage: kdna init <name>');
33
+ console.error('Error: Domain name required. Usage: kdna dev scaffold <name>');
33
34
  process.exit(1);
34
35
  }
35
36
 
@@ -56,8 +57,15 @@ function cmdInit(name) {
56
57
  'YYYY-MM-DD': today,
57
58
  });
58
59
 
59
- console.log(`✓ Created KDNA domain: ${targetDir}/`);
60
+ if (options.deprecatedAlias) {
61
+ console.warn(
62
+ 'Warning: kdna init is deprecated. Use kdna dev scaffold for dev source workspaces.',
63
+ );
64
+ }
65
+ console.log(`✓ Created non-canonical KDNA dev source workspace: ${targetDir}/`);
60
66
  console.log(` Files: KDNA_Core.json, KDNA_Patterns.json, kdna.json, tests/before-after.json`);
67
+ console.log(' This workspace is not a trusted KDNA asset.');
68
+ console.log(' To create a trusted .kdna asset, use KDNA Studio compile/export.');
61
69
 
62
70
  // Run structural validation (lint + schema only). Content quality checks
63
71
  // are for publish time, not scaffold time — the template contains placeholders
@@ -92,8 +100,8 @@ function cmdInit(name) {
92
100
  );
93
101
  console.log(` 3. Edit ${targetDir}/kdna.json — set author, description, repo`);
94
102
  console.log(` 4. Run: kdna dev validate ${name} (structural check)`);
95
- console.log(` 5. Run: kdna publish --check ${name} (content quality gate)`);
96
- console.log(` 6. Run: kdna verify ${name} (full judgment scoring)`);
103
+ console.log(` 5. Run: kdna publish --check ${name} (dev readiness check only)`);
104
+ console.log(` 6. Use KDNA Studio to Human Lock, compile, and export a trusted .kdna asset`);
97
105
  }
98
106
 
99
107
  /**
package/src/install.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  sha256File,
28
28
  readContainer,
29
29
  readContainerJson,
30
+ readContainerEntry,
30
31
  verifyAsset,
31
32
  } = require('./package-store');
32
33
 
@@ -153,6 +154,24 @@ function ensureDir(dir) {
153
154
  fs.mkdirSync(dir, { recursive: true });
154
155
  }
155
156
 
157
+ // ─── Audit Log ───────────────────────────────────────────────────────
158
+
159
+ function auditLog(action, details) {
160
+ try {
161
+ const logDir = path.join(USER_KDNA_DIR, 'logs');
162
+ ensureDir(logDir);
163
+ const logFile = path.join(logDir, 'audit.log');
164
+ const entry = JSON.stringify({
165
+ timestamp: new Date().toISOString(),
166
+ action,
167
+ ...details,
168
+ });
169
+ fs.appendFileSync(logFile, entry + '\n');
170
+ } catch {
171
+ /* audit is best-effort, never block on it */
172
+ }
173
+ }
174
+
156
175
  // ─── Source parsing ─────────────────────────────────────────────────────
157
176
 
158
177
  function parseSource(input) {
@@ -306,13 +325,14 @@ function cmdInstallExtended(input, args = []) {
306
325
 
307
326
  const yes = args.includes('--yes');
308
327
  const jsonMode = args.includes('--json');
328
+ const trusted = args.includes('--trusted');
309
329
  const source = parseSource(input);
310
330
 
311
331
  switch (source.type) {
312
332
  case 'registry':
313
333
  return installFromRegistry(source.parsed, yes, jsonMode);
314
334
  case 'local-file':
315
- return installFromLocalFile(source.path, yes, jsonMode);
335
+ return installFromLocalFile(source.path, yes, jsonMode, trusted);
316
336
  }
317
337
  }
318
338
 
@@ -411,6 +431,7 @@ function installSingleFromUrl({ entry, scope }, jsonMode = false) {
411
431
 
412
432
  verifySignature({ assetPath: tmpFile, scope, entry, lenient: true });
413
433
 
434
+ auditLog('install', { name: entry.name, version: entry.version, source: 'registry' });
414
435
  const installed = installAsset({
415
436
  sourcePath: tmpFile,
416
437
  name: entry.name,
@@ -483,7 +504,7 @@ function installCluster(clusterEntry, resolver, _yes, jsonMode = false) {
483
504
  }
484
505
  }
485
506
 
486
- function installFromLocalFile(filePath, _yes, jsonMode = false) {
507
+ function installFromLocalFile(filePath, yes, jsonMode = false, trusted = false) {
487
508
  const abs = path.resolve(filePath);
488
509
  if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) error(`Not a file: ${abs}`);
489
510
  if (!abs.endsWith('.kdna')) error(`Not a .kdna asset: ${abs}`, EXIT.INPUT_ERROR);
@@ -493,6 +514,98 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
493
514
  if (!declared || !/^@[a-z][a-z0-9-]*\/[a-z][a-z0-9_]*$/.test(declared)) {
494
515
  error(scopedNameError('package kdna.json.name', declared), EXIT.INPUT_ERROR);
495
516
  }
517
+
518
+ // ── Trust checks for local install ──────────────────────────
519
+ const trustLevel = { label: 'local_unverified', issues: [] };
520
+
521
+ // Check mimetype (plain text, not JSON)
522
+ try {
523
+ const mimeEntry = readContainerEntry(abs, 'mimetype');
524
+ const hasMimetype =
525
+ mimeEntry && mimeEntry.toString().trim() === 'application/vnd.aikdna.kdna+zip';
526
+ if (!hasMimetype) trustLevel.issues.push('missing or incorrect mimetype');
527
+ } catch {
528
+ trustLevel.issues.push('no mimetype entry');
529
+ }
530
+
531
+ // Check for KDNA_Core.json (may be encrypted — skip if unreadable)
532
+ try {
533
+ const hasCore = readContainerJson(abs, 'KDNA_Core.json');
534
+ if (!hasCore) trustLevel.issues.push('missing KDNA_Core.json');
535
+ } catch {
536
+ trustLevel.issues.push('KDNA_Core.json unreadable (may be encrypted)');
537
+ }
538
+
539
+ // Check content_digest
540
+ if (!manifest.content_digest) trustLevel.issues.push('no content_digest');
541
+
542
+ // Check authoring provenance
543
+ if (!manifest.authoring?.created_by) {
544
+ trustLevel.issues.push('no authoring provenance (not Studio-compiled)');
545
+ } else if (manifest.authoring.created_by === 'manual-dev-source') {
546
+ trustLevel.issues.push('created by manual dev source (not Studio-compiled)');
547
+ }
548
+
549
+ // Try signature verification if present
550
+ if (manifest.signature) {
551
+ try {
552
+ const sigResult = verifyAsset(abs, { requireSignature: true });
553
+ if (sigResult.signature_valid) {
554
+ trustLevel.label = 'local_signature_verified';
555
+ trustLevel.issues = trustLevel.issues.filter((i) => !i.includes('not Studio-compiled'));
556
+ } else {
557
+ trustLevel.issues.push('signature present but failed verification');
558
+ }
559
+ } catch {
560
+ trustLevel.issues.push('signature verification failed');
561
+ }
562
+ }
563
+
564
+ // --trusted mode: signature must be present and verified
565
+ if (trusted && trustLevel.issues.length > 0) {
566
+ const reasons = trustLevel.issues.map((i) => ` - ${i}`).join('\n');
567
+ error(
568
+ `Trust verification failed for local .kdna asset:\n${reasons}\n\n` +
569
+ `Use 'kdna install <file.kdna>' without --trusted to install anyway (unverified local asset).`,
570
+ EXIT.TRUST_FAILED,
571
+ );
572
+ }
573
+ // Signature is required for --trusted mode
574
+ if (trusted && !manifest.signature) {
575
+ error(
576
+ '--trusted requires a signed .kdna asset. This asset has no signature.\n' +
577
+ 'Use Studio compile/export with --sign, or install without --trusted.',
578
+ EXIT.TRUST_FAILED,
579
+ );
580
+ }
581
+ // For tested+ quality_badge, require Studio-compatible authoring provenance
582
+ const highTrustBadges = new Set(['tested', 'validated', 'expert_reviewed', 'production_ready']);
583
+ if (
584
+ trusted &&
585
+ highTrustBadges.has(manifest.quality_badge) &&
586
+ (!manifest.authoring?.compiler || !manifest.authoring?.compiler_version)
587
+ ) {
588
+ error(
589
+ `--trusted requires Studio-compatible authoring provenance for quality_badge "${manifest.quality_badge}".\n` +
590
+ 'This asset lacks compiler provenance. Re-publish through Studio pipeline.',
591
+ EXIT.TRUST_FAILED,
592
+ );
593
+ }
594
+
595
+ if (!jsonMode) {
596
+ if (trustLevel.label === 'local_signature_verified') {
597
+ console.log(` Trust: ${trustLevel.label}`);
598
+ } else {
599
+ console.warn(` Trust: ${trustLevel.label} — ${trustLevel.issues.join('; ')}`);
600
+ }
601
+ }
602
+
603
+ auditLog('install', {
604
+ name: declared,
605
+ version: manifest.version,
606
+ source: 'local-file',
607
+ path: abs,
608
+ });
496
609
  const installed = installAsset({
497
610
  sourcePath: abs,
498
611
  name: declared,
@@ -524,6 +637,7 @@ function installFromLocalFile(filePath, _yes, jsonMode = false) {
524
637
  function cmdRemove(input) {
525
638
  const parsed = parseName(input);
526
639
  if (!parsed) error(`Invalid name "${input}". Use @scope/name or bare name.`);
640
+ auditLog('remove', { name: parsed.full });
527
641
  if (!removeInstalled(parsed.full)) {
528
642
  console.log(`${parsed.full} is not installed.`);
529
643
  return;
@@ -0,0 +1,42 @@
1
+ // KDNA Key Derivation Parameters — canonical reference
2
+ // Per RFC-0008 and RFC-0009, these parameters MUST be used for all KDF operations.
3
+
4
+ const KDF_PARAMS = {
5
+ 'kdna-password-protected-v1': {
6
+ algorithm: 'Argon2id',
7
+ version: 0x13,
8
+ memoryCostKiB: 65536, // 64 MiB
9
+ timeCost: 3, // 3 iterations
10
+ parallelism: 4, // 4 lanes
11
+ saltLength: 32, // 256-bit random salt
12
+ hashLength: 32, // 256-bit derived key
13
+ tagLength: 16, // AES-256-GCM authentication tag
14
+ },
15
+ 'kdna-licensed-entry-v1': {
16
+ algorithm: 'HKDF-SHA256',
17
+ info: 'kdna-licensed-entry-v1',
18
+ salt: null, // No salt — deterministic from master key
19
+ keyLength: 32, // 256-bit AES key
20
+ wrapAlgorithm: 'AES-256-KW',
21
+ wireTagSize: 16, // Tag prepended to ciphertext in wire format
22
+ contentEncryption: 'AES-256-GCM',
23
+ },
24
+ 'kdna-identity-backup-v1': {
25
+ algorithm: 'PBKDF2-SHA256',
26
+ iterations: 100000,
27
+ keyLength: 32, // 256-bit AES key
28
+ ivLength: 16, // 128-bit random IV
29
+ encryption: 'AES-256-CBC',
30
+ },
31
+ };
32
+
33
+ function validateParameters(profile) {
34
+ const params = KDF_PARAMS[profile];
35
+ if (!params)
36
+ throw new Error(
37
+ `Unknown KDF profile: ${profile}. Valid: ${Object.keys(KDF_PARAMS).join(', ')}`,
38
+ );
39
+ return params;
40
+ }
41
+
42
+ module.exports = { KDF_PARAMS, validateParameters };
@@ -11,6 +11,25 @@ if (typeof core.createKdnaAssetReader !== 'function') {
11
11
 
12
12
  const assetReader = core.createKdnaAssetReader();
13
13
 
14
+ const V1_ENTRIES = [
15
+ 'KDNA_Core.json',
16
+ 'KDNA_Patterns.json',
17
+ 'KDNA_Scenarios.json',
18
+ 'KDNA_Cases.json',
19
+ 'KDNA_Reasoning.json',
20
+ 'KDNA_Evolution.json',
21
+ ];
22
+
23
+ function validateContainerV2(asset, assetPath) {
24
+ const hasPayload = asset.entries.has('payload.kdnab');
25
+ if (hasPayload) return; // v2, OK
26
+ const hasV1 = V1_ENTRIES.some((e) => asset.entries.has(e));
27
+ if (!hasV1) return; // neither v1 nor v2, probably empty — let caller decide
28
+ const found = V1_ENTRIES.filter((e) => asset.entries.has(e));
29
+ const msg = `ERR_LEGACY_PLAINTEXT_CONTAINER: This .kdna uses the removed v1 plaintext ZIP format (found: ${found.join(', ')}). Rebuild from source with KDNA Container v2.`;
30
+ throw Object.assign(new Error(msg), { code: 'ERR_LEGACY_PLAINTEXT_CONTAINER' });
31
+ }
32
+
14
33
  const INDEX_VERSION = 2;
15
34
 
16
35
  function ensureDir(dir) {
@@ -78,14 +97,16 @@ function listContainerEntries(kdnaPath) {
78
97
 
79
98
  function readContainer(kdnaPath, options = {}) {
80
99
  const asset = assetReader.openSync(kdnaPath);
100
+ validateContainerV2(asset, kdnaPath);
101
+ const dataMap = assetReader.readDataMapSync(asset, undefined, options);
81
102
  return {
82
- manifest: assetReader.readJsonSync(asset, 'kdna.json'),
83
- core: assetReader.readJsonSync(asset, 'KDNA_Core.json', options),
84
- patterns: assetReader.readJsonSync(asset, 'KDNA_Patterns.json', options),
85
- scenarios: assetReader.readJsonSync(asset, 'KDNA_Scenarios.json', options),
86
- cases: assetReader.readJsonSync(asset, 'KDNA_Cases.json', options),
87
- reasoning: assetReader.readJsonSync(asset, 'KDNA_Reasoning.json', options),
88
- evolution: assetReader.readJsonSync(asset, 'KDNA_Evolution.json', options),
103
+ manifest: dataMap['kdna.json'] || {},
104
+ core: dataMap['KDNA_Core.json'] || {},
105
+ patterns: dataMap['KDNA_Patterns.json'] || {},
106
+ scenarios: dataMap['KDNA_Scenarios.json'] || null,
107
+ cases: dataMap['KDNA_Cases.json'] || null,
108
+ reasoning: dataMap['KDNA_Reasoning.json'] || null,
109
+ evolution: dataMap['KDNA_Evolution.json'] || null,
89
110
  files: assetReader.listEntriesSync(asset),
90
111
  };
91
112
  }
@@ -222,6 +243,7 @@ module.exports = {
222
243
  readContainer,
223
244
  readContainerEntry,
224
245
  readContainerJson,
246
+ readAssetManifest,
225
247
  listContainerEntries,
226
248
  verifyAsset,
227
249
  getInstalled,
package/src/paths.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // KDNA shared path configuration — canonical source for ~/.kdna structure
2
2
  // Spec: docs/local-kdna-home-spec.md
3
+ // NOTE: domains/ is NOT part of the runtime model (see local-kdna-home-spec.md §Invariants).
4
+ // The domains field below is retained ONLY for legacy migration. New code MUST use packages/.
3
5
 
4
6
  const path = require('path');
5
7
 
@@ -10,14 +12,13 @@ const PATHS = {
10
12
  root: KDNA_HOME,
11
13
  config: path.join(KDNA_HOME, 'config.json'),
12
14
  identity: path.join(KDNA_HOME, 'identity'),
15
+ // LEGACY — domains/ is not part of the runtime model. Retained for migration only.
13
16
  domains: {
14
17
  root: path.join(KDNA_HOME, 'domains'),
15
18
  official: path.join(KDNA_HOME, 'domains', 'official'),
16
19
  local: path.join(KDNA_HOME, 'domains', 'local'),
17
20
  private: path.join(KDNA_HOME, 'domains', 'private'),
18
- // Legacy flat path — used for migration only
19
21
  legacy: path.join(KDNA_HOME, 'domains'),
20
- // All three directories for scanning
21
22
  all: [
22
23
  path.join(KDNA_HOME, 'domains', 'official'),
23
24
  path.join(KDNA_HOME, 'domains', 'local'),