@aikdna/kdna-cli 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/src/verify.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * kdna verify <name> — Quality signal across three layers.
2
+ * kdna verify <name|file.kdna> — Quality signal across three layers.
3
3
  *
4
4
  * --structure files exist, schema OK
5
- * --trust sha256 + Ed25519 signature against scope trust key
5
+ * --trust asset digest + Ed25519 signature against scope trust key
6
6
  * --judgment v2.1 governance fields (boundary, applies_when, eval cases)
7
7
  *
8
8
  * No flag = run all three.
@@ -17,19 +17,62 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
- const crypto = require('crypto');
21
- const { RegistryResolver, parseName } = require('./registry');
22
- const { EXIT } = require('./cmds/_common');
23
-
24
- let validateManifestFn;
25
- try {
26
- validateManifestFn = require('@aikdna/kdna-core').validateManifest;
27
- } catch {
28
- // kdna-core not available — manifest validation skipped
29
- }
20
+ const { RegistryResolver, parseName, registryTrustIssues, isEntryRevoked } = require('./registry');
21
+ const { EXIT, isYesNoSelfCheck } = require('./cmds/_common');
22
+ const { licenseDecryptOptionsForManifest } = require('./cmds/license');
23
+
24
+ const {
25
+ getInstalled,
26
+ listContainerEntries,
27
+ readContainerEntry,
28
+ readContainerJson,
29
+ resolveAsset,
30
+ verifyAsset,
31
+ } = require('./package-store');
32
+
33
+ function validateManifestFn(manifest) {
34
+ const errors = [];
35
+ const warnings = [];
36
+ const required = [
37
+ 'format',
38
+ 'format_version',
39
+ 'spec_version',
40
+ 'name',
41
+ 'version',
42
+ 'judgment_version',
43
+ 'description',
44
+ 'author',
45
+ 'license',
46
+ 'status',
47
+ 'quality_badge',
48
+ 'access',
49
+ 'languages',
50
+ 'default_language',
51
+ ];
30
52
 
31
- const USER_KDNA_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.kdna');
32
- const INSTALL_DIR = path.join(USER_KDNA_DIR, 'domains');
53
+ if (manifest.kdna_spec) errors.push('kdna.json: kdna_spec is not allowed. Use spec_version.');
54
+ if (manifest.language)
55
+ errors.push('kdna.json: language is not allowed. Use default_language and languages.');
56
+ for (const field of required) {
57
+ if (!(field in manifest) || manifest[field] === undefined || manifest[field] === '') {
58
+ errors.push(`kdna.json: missing required field "${field}"`);
59
+ }
60
+ }
61
+ if (manifest.format && manifest.format !== 'kdna') {
62
+ errors.push(`kdna.json.format: invalid value "${manifest.format}". Expected "kdna".`);
63
+ }
64
+ if (manifest.format_version && manifest.format_version !== '1.0') {
65
+ errors.push(
66
+ `kdna.json.format_version: invalid value "${manifest.format_version}". Expected "1.0".`,
67
+ );
68
+ }
69
+ if (manifest.spec_version && manifest.spec_version !== '1.0-rc') {
70
+ warnings.push(
71
+ `kdna.json.spec_version: non-standard value "${manifest.spec_version}". Expected "1.0-rc".`,
72
+ );
73
+ }
74
+ return { errors, warnings };
75
+ }
33
76
 
34
77
  function readJson(p) {
35
78
  try {
@@ -39,11 +82,84 @@ function readJson(p) {
39
82
  }
40
83
  }
41
84
 
85
+ function directoryView(root) {
86
+ return {
87
+ kind: 'directory',
88
+ exists(name) {
89
+ return fs.existsSync(path.join(root, name));
90
+ },
91
+ readJson(name) {
92
+ return readJson(path.join(root, name));
93
+ },
94
+ readText(name) {
95
+ try {
96
+ return fs.readFileSync(path.join(root, name), 'utf8');
97
+ } catch {
98
+ return '';
99
+ }
100
+ },
101
+ listDirFiles(dirName) {
102
+ const dir = path.join(root, dirName);
103
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
104
+ return fs.readdirSync(dir);
105
+ },
106
+ };
107
+ }
108
+
109
+ function assetView(kdnaPath, options = {}) {
110
+ const entries = new Set(listContainerEntries(kdnaPath));
111
+ return {
112
+ kind: 'asset',
113
+ path: kdnaPath,
114
+ exists(name) {
115
+ return entries.has(name);
116
+ },
117
+ readJson(name) {
118
+ return readContainerJson(kdnaPath, name, options);
119
+ },
120
+ readText(name) {
121
+ if (!entries.has(name)) return '';
122
+ return readContainerEntry(kdnaPath, name).toString('utf8');
123
+ },
124
+ listDirFiles(dirName) {
125
+ const prefix = `${dirName.replace(/\/+$/, '')}/`;
126
+ const files = [];
127
+ for (const entryName of entries) {
128
+ if (!entryName.startsWith(prefix)) continue;
129
+ const rest = entryName.slice(prefix.length);
130
+ if (rest && !rest.includes('/')) files.push(rest);
131
+ }
132
+ return files;
133
+ },
134
+ };
135
+ }
136
+
137
+ function asView(input, options = {}) {
138
+ if (input && typeof input.exists === 'function') return input;
139
+ return directoryView(input, options);
140
+ }
141
+
142
+ function readJsonFromView(view, entryName, issues = null) {
143
+ try {
144
+ return view.readJson(entryName);
145
+ } catch (e) {
146
+ if (issues) issues.push({ severity: 'error', msg: `${entryName}: ${e.message}` });
147
+ return null;
148
+ }
149
+ }
150
+
42
151
  // ─── Structure layer ───────────────────────────────────────────────────
43
152
 
44
- function checkStructure(destDir) {
153
+ function checkStructure(input, options = {}) {
154
+ const view = asView(input);
45
155
  const issues = [];
46
156
  const passed = [];
157
+ if (options.licenseError) {
158
+ issues.push({
159
+ severity: 'error',
160
+ msg: `license required to verify encrypted entries: ${options.licenseError}`,
161
+ });
162
+ }
47
163
 
48
164
  const required = ['KDNA_Core.json', 'KDNA_Patterns.json', 'kdna.json'];
49
165
  const optional = [
@@ -54,7 +170,7 @@ function checkStructure(destDir) {
54
170
  ];
55
171
 
56
172
  for (const f of required) {
57
- if (!fs.existsSync(path.join(destDir, f))) {
173
+ if (!view.exists(f)) {
58
174
  issues.push({ severity: 'error', msg: `required file missing: ${f}` });
59
175
  } else {
60
176
  passed.push(`has ${f}`);
@@ -63,7 +179,7 @@ function checkStructure(destDir) {
63
179
 
64
180
  // Validate kdna.json against canonical manifest schema
65
181
  if (validateManifestFn) {
66
- const manifest = readJson(path.join(destDir, 'kdna.json'));
182
+ const manifest = readJsonFromView(view, 'kdna.json', issues);
67
183
  if (manifest) {
68
184
  const mResult = validateManifestFn(manifest);
69
185
  for (const e of mResult.errors) issues.push({ severity: 'error', msg: e });
@@ -73,12 +189,12 @@ function checkStructure(destDir) {
73
189
  }
74
190
 
75
191
  for (const f of optional) {
76
- if (fs.existsSync(path.join(destDir, f))) passed.push(`has ${f}`);
192
+ if (view.exists(f)) passed.push(`has ${f}`);
77
193
  }
78
194
 
79
195
  // Schema check using kdna-core if available
80
196
  try {
81
- const core = readJson(path.join(destDir, 'KDNA_Core.json'));
197
+ const core = options.licenseError ? null : readJsonFromView(view, 'KDNA_Core.json', issues);
82
198
  if (core) {
83
199
  if (!core.axioms || !Array.isArray(core.axioms) || core.axioms.length === 0) {
84
200
  issues.push({ severity: 'error', msg: 'KDNA_Core.axioms missing or empty' });
@@ -90,7 +206,7 @@ function checkStructure(destDir) {
90
206
  issues.push({ severity: 'warn', msg: 'KDNA_Core.stances missing or empty' });
91
207
  }
92
208
  }
93
- const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
209
+ const pat = options.licenseError ? null : readJsonFromView(view, 'KDNA_Patterns.json', issues);
94
210
  if (pat) {
95
211
  if (!pat.misunderstandings || pat.misunderstandings.length === 0) {
96
212
  issues.push({ severity: 'warn', msg: 'KDNA_Patterns.misunderstandings missing or empty' });
@@ -111,11 +227,12 @@ function checkStructure(destDir) {
111
227
 
112
228
  // ─── Trust layer ───────────────────────────────────────────────────────
113
229
 
114
- function checkTrust(destDir, scope, entry) {
230
+ function checkTrust(input, scope, entry, options = {}) {
231
+ const view = asView(input);
115
232
  const issues = [];
116
233
  const passed = [];
117
234
 
118
- const manifest = readJson(path.join(destDir, 'kdna.json'));
235
+ const manifest = readJsonFromView(view, 'kdna.json', issues);
119
236
  if (!manifest) {
120
237
  issues.push({ severity: 'error', msg: 'kdna.json missing — cannot verify trust' });
121
238
  return { layer: 'trust', issues, passed };
@@ -151,58 +268,56 @@ function checkTrust(destDir, scope, entry) {
151
268
  });
152
269
  } else {
153
270
  passed.push('embedded public_key_pem present');
271
+ }
154
272
 
155
- // Recompute fingerprint
156
- const fp =
157
- 'ed25519:' + crypto.createHash('sha256').update(manifest.author.public_key_pem).digest('hex');
158
- if (fp !== manifest.author.pubkey) {
273
+ if (options.assetPath) {
274
+ const verification = verifyAsset(options.assetPath, { requireSignature: true });
275
+ for (const warning of verification.warnings || []) {
276
+ if (!/signature missing/.test(warning)) issues.push({ severity: 'warn', msg: warning });
277
+ }
278
+ for (const err of verification.errors || []) {
279
+ if (/signature|author\.|public_key|pubkey|fingerprint|Ed25519/i.test(err)) {
280
+ issues.push({ severity: 'error', msg: err });
281
+ }
282
+ }
283
+ if (verification.signature_valid === true) {
284
+ passed.push('Ed25519 signature VALID over canonical payload');
285
+ } else if (manifest.signature) {
159
286
  issues.push({
160
287
  severity: 'error',
161
- msg: 'embedded PEM does not match author.pubkey fingerprint',
288
+ msg: 'Ed25519 signature INVALID or unavailable',
162
289
  });
163
- } else {
164
- passed.push('PEM fingerprint matches author.pubkey');
165
-
166
- // Full Ed25519 verify
167
- try {
168
- const files = fs
169
- .readdirSync(destDir)
170
- .filter((f) => f.endsWith('.json'))
171
- .sort();
172
- const parts = [];
173
- for (const f of files) {
174
- const full = path.join(destDir, f);
175
- let buf;
176
- if (f === 'kdna.json') {
177
- const obj = JSON.parse(fs.readFileSync(full, 'utf8'));
178
- delete obj.signature;
179
- delete obj._source;
180
- buf = Buffer.from(JSON.stringify(obj));
181
- } else {
182
- buf = fs.readFileSync(full);
183
- }
184
- const hash = crypto.createHash('sha256').update(buf).digest('hex');
185
- parts.push(`${f}:${hash}`);
186
- }
187
- const payload = parts.join('\n');
188
- const sigHex = manifest.signature.replace(/^ed25519:/, '');
189
- const publicKey = crypto.createPublicKey(manifest.author.public_key_pem);
190
- const ok = crypto.verify(null, Buffer.from(payload), publicKey, Buffer.from(sigHex, 'hex'));
191
- if (ok) passed.push('Ed25519 signature VALID over canonical payload');
192
- else
193
- issues.push({
194
- severity: 'error',
195
- msg: 'Ed25519 signature INVALID — package may be tampered',
196
- });
197
- } catch (e) {
198
- issues.push({ severity: 'error', msg: `signature verify failed: ${e.message}` });
199
- }
200
290
  }
201
291
  }
202
292
 
203
- // 4. sha256 vs registry (if entry provided)
204
- if (entry?.sha256) {
205
- passed.push(`registry sha256 declared: ${entry.sha256.slice(0, 16)}…`);
293
+ // 4. asset digest vs registry (if entry provided)
294
+ const registry = options.registry || null;
295
+ const registryIssues = registry ? registryTrustIssues(registry) : [];
296
+ for (const issue of registryIssues) {
297
+ issues.push({ severity: 'error', msg: issue });
298
+ }
299
+
300
+ const revocation = registry && entry ? isEntryRevoked(registry, entry) : null;
301
+ if (revocation) {
302
+ issues.push({
303
+ severity: 'error',
304
+ msg: `registry revokes this asset${revocation.reason ? `: ${revocation.reason}` : ''}`,
305
+ });
306
+ }
307
+
308
+ const registryDigest = entry?.asset_digest || null;
309
+ if (registryDigest) {
310
+ passed.push(`registry asset_digest declared: ${registryDigest.slice(0, 23)}…`);
311
+ if (options.assetDigest && options.assetDigest !== registryDigest) {
312
+ issues.push({
313
+ severity: 'error',
314
+ msg: `asset digest mismatch: registry ${registryDigest}, local ${options.assetDigest}`,
315
+ });
316
+ } else if (options.assetDigest) {
317
+ passed.push('local asset_digest matches registry');
318
+ }
319
+ } else if (entry) {
320
+ issues.push({ severity: 'error', msg: 'registry asset_digest missing' });
206
321
  }
207
322
 
208
323
  // 5. scope governance
@@ -218,7 +333,8 @@ function checkTrust(destDir, scope, entry) {
218
333
 
219
334
  // ─── Judgment layer ────────────────────────────────────────────────────
220
335
 
221
- function checkJudgment(destDir) {
336
+ function checkJudgment(input, options = {}) {
337
+ const view = asView(input);
222
338
  const issues = [];
223
339
  const passed = [];
224
340
  const score = { total: 0, max: 0 };
@@ -231,17 +347,23 @@ function checkJudgment(destDir) {
231
347
  else issues.push({ severity: 'warn', msg: `missing: ${label}` });
232
348
  }
233
349
 
234
- const core = readJson(path.join(destDir, 'KDNA_Core.json'));
235
- const pat = readJson(path.join(destDir, 'KDNA_Patterns.json'));
236
- const manifest = readJson(path.join(destDir, 'kdna.json'));
350
+ if (options.licenseError) {
351
+ issues.push({
352
+ severity: 'error',
353
+ msg: `license required to verify encrypted judgment: ${options.licenseError}`,
354
+ });
355
+ }
356
+
357
+ const core = options.licenseError ? null : readJsonFromView(view, 'KDNA_Core.json', issues);
358
+ const pat = options.licenseError ? null : readJsonFromView(view, 'KDNA_Patterns.json', issues);
359
+ const manifest = readJsonFromView(view, 'kdna.json', issues);
237
360
 
238
361
  // 1. Boundary declaration in README (REQUIRED)
239
362
  // Either classic "## Scope" + "## Out of Scope" pair,
240
363
  // OR v2.1 "Four Questions" section (#2 = applies, #4 = does not).
241
- const readmePath = path.join(destDir, 'README.md');
242
364
  let readme = '';
243
365
  try {
244
- readme = fs.readFileSync(readmePath, 'utf8');
366
+ readme = view.readText('README.md');
245
367
  } catch {
246
368
  /* ok */
247
369
  }
@@ -330,16 +452,16 @@ function checkJudgment(destDir) {
330
452
  // 4. self_check format: yes/no questions
331
453
  if (pat?.self_check) {
332
454
  const total = pat.self_check.length;
333
- const yn = pat.self_check.filter((q) => typeof q === 'string' && q.trim().endsWith('?')).length;
334
- bump(total, yn, `self_check questions ending in "?" (${yn}/${total})`);
455
+ const yn = pat.self_check.filter((q) => isYesNoSelfCheck(q)).length;
456
+ bump(total, yn, `self_check yes/no questions (${yn}/${total})`);
335
457
  if (total < 3)
336
458
  issues.push({ severity: 'warn', msg: `only ${total} self_check entries (recommend ≥3)` });
337
459
  }
338
460
 
339
461
  // 5. eval cases present (REQUIRED: ≥4 cases)
340
- const evalDir = path.join(destDir, 'evals');
341
- if (fs.existsSync(evalDir)) {
342
- const files = fs.readdirSync(evalDir).filter((f) => f.endsWith('.json'));
462
+ const evalFiles = view.listDirFiles('evals').filter((f) => f.endsWith('.json'));
463
+ if (evalFiles.length) {
464
+ const files = evalFiles;
343
465
  if (files.length >= 4) {
344
466
  bump(2, 2, `evals/ directory has ${files.length} case files`);
345
467
  } else if (files.length > 0) {
@@ -399,10 +521,11 @@ function renderLayer(result) {
399
521
 
400
522
  // ─── I18N layer ──────────────────────────────────────────────────────
401
523
 
402
- function checkI18n(destDir) {
524
+ function checkI18n(input) {
525
+ const view = asView(input);
403
526
  const issues = [];
404
527
  const passed = [];
405
- const manifest = readJson(path.join(destDir, 'kdna.json')) || {};
528
+ const manifest = readJsonFromView(view, 'kdna.json', issues) || {};
406
529
  const languages = manifest.languages || [];
407
530
  const i18nLevel = manifest.i18n_level || 'L0';
408
531
 
@@ -417,14 +540,13 @@ function checkI18n(destDir) {
417
540
  const canonical = manifest.default_language || languages[0] || 'en';
418
541
  for (const lang of languages) {
419
542
  if (lang === canonical) continue;
420
- const localeDir = path.join(destDir, 'locales', lang);
421
543
 
422
544
  // L1: card + readme
423
545
  if (['L1', 'L2', 'L3', 'L4'].includes(i18nLevel)) {
424
- if (!fs.existsSync(path.join(localeDir, 'KDNA_CARD.json'))) {
546
+ if (!view.exists(`locales/${lang}/KDNA_CARD.json`)) {
425
547
  issues.push({ severity: 'error', msg: `i18n: ${lang} KDNA_CARD.json missing` });
426
548
  } else {
427
- const card = readJson(path.join(localeDir, 'KDNA_CARD.json'));
549
+ const card = readJsonFromView(view, `locales/${lang}/KDNA_CARD.json`, issues);
428
550
  if (card) {
429
551
  passed.push(`locales/${lang}/KDNA_CARD.json OK`);
430
552
  if (!card.display_name)
@@ -433,7 +555,7 @@ function checkI18n(destDir) {
433
555
  issues.push({ severity: 'warn', msg: `i18n: ${lang} card missing intended_use` });
434
556
  }
435
557
  }
436
- if (!fs.existsSync(path.join(localeDir, 'README.md'))) {
558
+ if (!view.exists(`locales/${lang}/README.md`)) {
437
559
  issues.push({ severity: 'warn', msg: `i18n: ${lang} README.md missing` });
438
560
  } else {
439
561
  passed.push(`locales/${lang}/README.md OK`);
@@ -442,13 +564,13 @@ function checkI18n(destDir) {
442
564
 
443
565
  // L2: overlay files
444
566
  if (['L2', 'L3', 'L4'].includes(i18nLevel)) {
445
- const coreOverlay = path.join(localeDir, 'KDNA_Core.overlay.json');
446
- if (!fs.existsSync(coreOverlay)) {
567
+ const coreOverlay = `locales/${lang}/KDNA_Core.overlay.json`;
568
+ if (!view.exists(coreOverlay)) {
447
569
  issues.push({ severity: 'error', msg: `i18n: ${lang} KDNA_Core.overlay.json missing` });
448
570
  } else {
449
- const overlay = readJson(coreOverlay);
571
+ const overlay = readJsonFromView(view, coreOverlay, issues);
450
572
  if (overlay?.translations) {
451
- const core = readJson(path.join(destDir, 'KDNA_Core.json'));
573
+ const core = readJsonFromView(view, 'KDNA_Core.json', issues);
452
574
  if (core?.axioms) {
453
575
  const validIds = new Set(core.axioms.map((a) => a.id));
454
576
  for (const key of Object.keys(overlay.translations)) {
@@ -466,7 +588,7 @@ function checkI18n(destDir) {
466
588
  );
467
589
  }
468
590
  }
469
- if (!fs.existsSync(path.join(localeDir, 'KDNA_Patterns.overlay.json'))) {
591
+ if (!view.exists(`locales/${lang}/KDNA_Patterns.overlay.json`)) {
470
592
  issues.push({ severity: 'warn', msg: `i18n: ${lang} KDNA_Patterns.overlay.json missing` });
471
593
  }
472
594
  }
@@ -487,12 +609,13 @@ function checkI18n(destDir) {
487
609
 
488
610
  // ─── Governance layer ───────────────────────────────────────────────
489
611
 
490
- function checkGovernance(destDir) {
612
+ function checkGovernance(input) {
613
+ const view = asView(input);
491
614
  const issues = [];
492
615
  const passed = [];
493
- const card = readJson(path.join(destDir, 'KDNA_CARD.json')) || {};
616
+ const card = readJsonFromView(view, 'KDNA_CARD.json', issues) || {};
494
617
 
495
- if (!readJson(path.join(destDir, 'KDNA_CARD.json'))) {
618
+ if (!card || !view.exists('KDNA_CARD.json')) {
496
619
  issues.push({ severity: 'error', msg: 'governance: KDNA_CARD.json missing — required' });
497
620
  return { layer: 'governance', passed: false, issues, results: issues.map((i) => i.msg) };
498
621
  }
@@ -579,22 +702,28 @@ function cmdVerify(input, args = []) {
579
702
  console.log(JSON.stringify({ ok: false, error: `Invalid name: ${input}` }));
580
703
  process.exit(EXIT.INPUT_ERROR);
581
704
  }
582
- const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
583
- if (!destDir || !require('fs').existsSync(destDir)) {
705
+ const installed = getInstalled(parsed.full);
706
+ if (!installed) {
584
707
  console.log(JSON.stringify({ ok: false, error: `Domain not installed: ${input}` }));
585
708
  process.exit(EXIT.INPUT_ERROR);
586
709
  }
587
710
  const { checkTrust: agentCheckTrust } = require('./agent');
588
711
  const trust = agentCheckTrust(parsed.full);
589
- console.log(JSON.stringify({
590
- domain: parsed.full,
591
- passed: trust.passed,
592
- failures: trust.failures,
593
- warnings: trust.warnings,
594
- risk_level: trust.riskLevel,
595
- spec_version: trust.specVersion,
596
- signature_valid: trust.signatureValid,
597
- }, null, 2));
712
+ console.log(
713
+ JSON.stringify(
714
+ {
715
+ domain: parsed.full,
716
+ passed: trust.passed,
717
+ failures: trust.failures,
718
+ warnings: trust.warnings,
719
+ risk_level: trust.riskLevel,
720
+ spec_version: trust.specVersion,
721
+ signature_valid: trust.signatureValid,
722
+ },
723
+ null,
724
+ 2,
725
+ ),
726
+ );
598
727
  process.exit(trust.passed ? 0 : EXIT.TRUST_FAILED);
599
728
  }
600
729
 
@@ -608,58 +737,81 @@ function cmdVerify(input, args = []) {
608
737
  const all = !want.structure && !want.trust && !want.judgment && !want.i18n && !want.governance;
609
738
  if (all) want.structure = want.trust = want.judgment = true;
610
739
 
611
- // Resolve name installed path + scope/entry
612
- const parsed = parseName(input);
613
- if (!parsed) {
740
+ // Resolve installed name or direct local .kdna asset path.
741
+ const asset = resolveAsset(input);
742
+ const parsed = asset?.parsed || parseName(asset?.name || '');
743
+ const displayName = parsed?.full || asset?.name || input;
744
+ if (!asset) {
614
745
  if (jsonMode) {
615
746
  console.log(
616
747
  JSON.stringify({
617
748
  name: input,
618
749
  ok: false,
619
- error: `Invalid name "${input}". Use @scope/name or bare name.`,
750
+ error: `KDNA asset not found: ${input}. Use an installed name or a .kdna file.`,
620
751
  }),
621
752
  );
622
753
  } else {
623
- console.error(`Invalid name "${input}". Use @scope/name or bare name.`);
624
- }
625
- process.exit(EXIT.INPUT_ERROR);
626
- }
627
-
628
- const destDir = path.join(INSTALL_DIR, parsed.scope, parsed.ident);
629
- if (!fs.existsSync(destDir)) {
630
- if (jsonMode) {
631
- console.log(
632
- JSON.stringify({
633
- name: parsed.full,
634
- ok: false,
635
- error: `${parsed.full} is not installed. Run: kdna install ${input}`,
636
- }),
637
- );
638
- } else {
639
- console.error(`${parsed.full} is not installed. Run: kdna install ${input}`);
754
+ console.error(`KDNA asset not found: ${input}. Use an installed name or a .kdna file.`);
640
755
  }
641
756
  process.exit(EXIT.INPUT_ERROR);
642
757
  }
643
758
 
644
759
  let scope = null,
645
- entry = null;
760
+ entry = null,
761
+ registry = null;
646
762
  if (want.trust) {
647
763
  try {
648
764
  const resolver = new RegistryResolver({ allowNetwork: false });
649
- const r = resolver.resolve(parsed.full);
650
- scope = r.scope;
651
- entry = r.entry;
765
+ if (parsed) {
766
+ const r = resolver.resolve(parsed.full);
767
+ scope = r.scope;
768
+ entry = r.entry;
769
+ registry = r.registry;
770
+ }
652
771
  } catch (e) {
653
772
  if (!jsonMode) console.warn(` ⚠ registry lookup failed: ${e.message.split('\n')[0]}`);
654
773
  }
655
774
  }
656
775
 
776
+ let decryptOptions = {};
777
+ let licenseError = null;
778
+ let manifest = null;
779
+ try {
780
+ manifest = readContainerJson(asset.asset_path, 'kdna.json') || {};
781
+ } catch (e) {
782
+ licenseError = e.message;
783
+ }
784
+
785
+ const encryptedEntries = Array.isArray(manifest?.encryption?.encrypted_entries)
786
+ ? manifest.encryption.encrypted_entries
787
+ : [];
788
+ const requiresProtectedRead =
789
+ encryptedEntries.length > 0 &&
790
+ (want.structure || want.judgment || want.i18n || want.governance);
791
+ if (requiresProtectedRead) {
792
+ const licensed = licenseDecryptOptionsForManifest(manifest);
793
+ if (licensed.ok) {
794
+ decryptOptions = { decryptEntry: licensed.decryptEntry };
795
+ } else {
796
+ licenseError = licensed.error;
797
+ }
798
+ }
799
+
800
+ const view = assetView(asset.asset_path, decryptOptions);
657
801
  const results = [];
658
- if (want.structure) results.push(checkStructure(destDir));
659
- if (want.trust) results.push(checkTrust(destDir, scope, entry));
660
- if (want.judgment) results.push(checkJudgment(destDir));
661
- if (want.i18n) results.push(checkI18n(destDir));
662
- if (want.governance) results.push(checkGovernance(destDir));
802
+ if (want.structure) results.push(checkStructure(view, { licenseError }));
803
+ if (want.trust) {
804
+ results.push(
805
+ checkTrust(view, scope, entry, {
806
+ registry,
807
+ assetDigest: asset.asset_digest || null,
808
+ assetPath: asset.asset_path,
809
+ }),
810
+ );
811
+ }
812
+ if (want.judgment) results.push(checkJudgment(view, { licenseError }));
813
+ if (want.i18n) results.push(checkI18n(view, { licenseError }));
814
+ if (want.governance) results.push(checkGovernance(view, { licenseError }));
663
815
 
664
816
  // ── JSON output ──────────────────────────────────────────────────────
665
817
  if (jsonMode) {
@@ -676,9 +828,6 @@ function cmdVerify(input, args = []) {
676
828
  const structureResult = results.find((r) => r.layer === 'structure');
677
829
  const trustResult = results.find((r) => r.layer === 'trust');
678
830
  const judgmentResult = results.find((r) => r.layer === 'judgment');
679
- const i18nResult = results.find((r) => r.layer === 'i18n');
680
- const governanceResult = results.find((r) => r.layer === 'governance');
681
-
682
831
  let exitCode = EXIT.OK;
683
832
  if (structureResult && structureResult.issues.some((i) => i.severity === 'error')) {
684
833
  exitCode = EXIT.VALIDATION_FAILED;
@@ -691,8 +840,10 @@ function cmdVerify(input, args = []) {
691
840
  console.log(
692
841
  JSON.stringify(
693
842
  {
694
- name: parsed.full,
695
- path: destDir,
843
+ name: displayName,
844
+ path: asset.asset_path,
845
+ asset_digest: asset.asset_digest || null,
846
+ content_digest: asset.content_digest || null,
696
847
  layers,
697
848
  ok: exitCode === EXIT.OK,
698
849
  },
@@ -705,8 +856,10 @@ function cmdVerify(input, args = []) {
705
856
 
706
857
  // ── Text output (default) ────────────────────────────────────────────
707
858
  console.log('═'.repeat(64));
708
- console.log(` Verify ${parsed.full}`);
709
- console.log(` Path: ${destDir}`);
859
+ console.log(` Verify ${displayName}`);
860
+ console.log(` Asset: ${asset.asset_path}`);
861
+ if (asset.asset_digest) console.log(` Asset digest: ${asset.asset_digest}`);
862
+ if (asset.content_digest) console.log(` Content digest: ${asset.content_digest}`);
710
863
  console.log('═'.repeat(64));
711
864
 
712
865
  for (const r of results) renderLayer(r);