multimodel-dev-os 3.2.0 → 3.5.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.
Files changed (68) hide show
  1. package/.ai/policies/registry-policy.yaml +29 -1
  2. package/.ai/registries/trusted-keys.yaml +12 -0
  3. package/.ai/schema/registry-manifest.schema.json +31 -2
  4. package/.ai/schema/registry-policy.schema.json +37 -1
  5. package/.ai/schema/trusted-keys.schema.json +69 -0
  6. package/AGENTS.md +22 -26
  7. package/MEMORY.md +34 -11
  8. package/README.md +1 -1
  9. package/RUNBOOK.md +28 -36
  10. package/TASKS.md +15 -5
  11. package/bin/multimodel-dev-os.js +1366 -548
  12. package/docs/.vitepress/config.js +3 -1
  13. package/docs/architecture.md +3 -1
  14. package/docs/index.md +5 -5
  15. package/docs/npm-publishing.md +5 -5
  16. package/docs/package-safety.md +3 -2
  17. package/docs/public/llms-full.txt +5 -1
  18. package/docs/public/llms.txt +6 -1
  19. package/docs/public/sitemap.xml +10 -0
  20. package/docs/registry-policy.md +29 -1
  21. package/docs/registry-security.md +73 -6
  22. package/docs/registry-signing.md +70 -0
  23. package/docs/registry-sync.md +5 -2
  24. package/docs/registry-trust-store.md +66 -0
  25. package/docs/release-policy.md +1 -1
  26. package/docs/security-threat-model.md +96 -0
  27. package/docs/testing.md +15 -2
  28. package/docs/trusted-registries.md +1 -1
  29. package/docs/v3-roadmap.md +11 -6
  30. package/docs/v3.5.0-readiness.md +46 -0
  31. package/package.json +1 -1
  32. package/scripts/install.ps1 +1 -1
  33. package/scripts/install.sh +1 -1
  34. package/scripts/verify.js +206 -9
  35. package/src/cli/help.js +1 -1
  36. package/src/cli/main.js +626 -81
  37. package/src/core/policy.js +9 -1
  38. package/src/registry/provenance.js +114 -0
  39. package/src/registry/signing.js +392 -0
  40. package/src/registry/trust-store.js +41 -0
  41. package/src/registry/verdict.js +51 -0
  42. package/tests/fixtures/signed-registries/README.md +4 -0
  43. package/tests/fixtures/signed-registries/revoked-key/catalog.yaml +8 -0
  44. package/tests/fixtures/signed-registries/revoked-key/expected-verdict.json +7 -0
  45. package/tests/fixtures/signed-registries/revoked-key/registry-manifest.yaml +14 -0
  46. package/tests/fixtures/signed-registries/tampered-manifest/catalog.yaml +8 -0
  47. package/tests/fixtures/signed-registries/tampered-manifest/expected-verdict.json +7 -0
  48. package/tests/fixtures/signed-registries/tampered-manifest/registry-manifest.yaml +14 -0
  49. package/tests/fixtures/signed-registries/trusted-keys.yaml +23 -0
  50. package/tests/fixtures/signed-registries/unsigned-remote-required/catalog.yaml +8 -0
  51. package/tests/fixtures/signed-registries/unsigned-remote-required/expected-verdict.json +7 -0
  52. package/tests/fixtures/signed-registries/unsigned-remote-required/registry-manifest.yaml +9 -0
  53. package/tests/fixtures/signed-registries/unsupported-algorithm/catalog.yaml +8 -0
  54. package/tests/fixtures/signed-registries/unsupported-algorithm/expected-verdict.json +7 -0
  55. package/tests/fixtures/signed-registries/unsupported-algorithm/registry-manifest.yaml +14 -0
  56. package/tests/fixtures/signed-registries/valid-signed-registry/catalog.yaml +8 -0
  57. package/tests/fixtures/signed-registries/valid-signed-registry/expected-verdict.json +7 -0
  58. package/tests/fixtures/signed-registries/valid-signed-registry/registry-manifest.yaml +14 -0
  59. package/tests/fixtures/signed-registries/wrong-key/catalog.yaml +8 -0
  60. package/tests/fixtures/signed-registries/wrong-key/expected-verdict.json +7 -0
  61. package/tests/fixtures/signed-registries/wrong-key/registry-manifest.yaml +14 -0
  62. package/tests/unit/registry-e2e-signature-fixtures.test.js +288 -0
  63. package/tests/unit/registry-policy.test.js +6 -0
  64. package/tests/unit/registry-provenance.test.js +185 -0
  65. package/tests/unit/registry-public-signing.test.js +109 -0
  66. package/tests/unit/registry-signature-policy.test.js +100 -0
  67. package/tests/unit/registry-signing.test.js +193 -0
  68. package/tests/unit/registry-trust-store.test.js +133 -0
package/src/cli/main.js CHANGED
@@ -20,6 +20,20 @@ import { loadRegistryPolicy } from '../core/policy.js';
20
20
  import { shouldIgnorePath, isSafePath } from '../core/security.js';
21
21
  import { validateRegistryUrl } from '../registry/validation.js';
22
22
  import { loadRegistrySources, saveRegistrySources } from '../registry/sources.js';
23
+ import { loadRegistryLockfile, saveRegistryLockfile, updateLockfileEntry, getLockfilePath } from '../registry/provenance.js';
24
+ import {
25
+ loadSigningKey,
26
+ generateSigningKey,
27
+ saveSigningKey,
28
+ signPayload,
29
+ verifySignature,
30
+ getSigningKeyPath,
31
+ verifySignatureBlock,
32
+ createCanonicalPayload,
33
+ normalizePublicKey
34
+ } from '../registry/signing.js';
35
+ import { loadTrustedKeys } from '../registry/trust-store.js';
36
+ import { createTrustVerdict } from '../registry/verdict.js';
23
37
  import { loadCatalog, loadCatalogFromSource, loadAllCatalogs } from '../catalog/loader.js';
24
38
  import { validatePluginManifest } from '../plugin/manifest.js';
25
39
  import { sourceRoot, version, loadTemplates, loadAdapters } from '../core/globals.js';
@@ -393,8 +407,30 @@ if (COMMAND === 'init') {
393
407
  console.error('\x1b[31mError: Please specify a cache subcommand: clear.\x1b[0m');
394
408
  process.exit(1);
395
409
  }
410
+ } else if (sub === 'keygen') {
411
+ handleRegistryKeygen(params);
412
+ } else if (sub === 'lock') {
413
+ handleRegistryLock(params);
414
+ } else if (sub === 'trust') {
415
+ const trustSub = positional[2];
416
+ if (trustSub === 'list') {
417
+ handleRegistryTrustList(params);
418
+ } else if (trustSub === 'show') {
419
+ const keyId = positional[3];
420
+ if (!keyId) {
421
+ console.error('\x1b[31mError: Please specify a key ID.\x1b[0m');
422
+ process.exit(1);
423
+ }
424
+ handleRegistryTrustShow(keyId, params);
425
+ } else if (trustSub === 'verify') {
426
+ handleRegistryTrustVerify(params);
427
+ } else {
428
+ console.error('\x1b[31mError: Please specify a trust subcommand: list, show, or verify.\x1b[0m');
429
+ console.log('Example: node bin/multimodel-dev-os.js registry trust list');
430
+ process.exit(1);
431
+ }
396
432
  } else {
397
- console.error('\x1b[31mError: Please specify a registry subcommand: list, add, remove, sync, status, verify, show, or cache.\x1b[0m');
433
+ console.error('\x1b[31mError: Please specify a registry subcommand: list, add, remove, sync, status, verify, show, cache, keygen, lock, or trust.\x1b[0m');
398
434
  console.log('Example: node bin/multimodel-dev-os.js registry list');
399
435
  process.exit(1);
400
436
  }
@@ -5106,22 +5142,30 @@ function handleRegistryList(options) {
5106
5142
  console.log('==================================================');
5107
5143
  console.log(`Policy Status: allow_remote_registries = \x1b[${policy.allow_remote_registries ? '32mtrue' : '33mfalse'}\x1b[0m (Remote registries are disabled by default for safety)\n`);
5108
5144
 
5145
+ const lockfile = loadRegistryLockfile(options.target || process.cwd());
5146
+
5109
5147
  sources.forEach(s => {
5110
5148
  const status = s.enabled ? '\x1b[32m● enabled\x1b[0m' : '\x1b[90m○ disabled\x1b[0m';
5111
5149
  const label = s.name === 'bundled' ? 'bundled' : s.type === 'local' ? `local:${s.name}` : `remote:${s.name}`;
5112
- console.log(` \x1b[32m${s.name}\x1b[0m [${label}] ${status}`);
5150
+ const lockEntry = lockfile.entries[s.name];
5151
+ const lockBadge = lockEntry
5152
+ ? (lockEntry.signature ? ' \x1b[32m[signed]\x1b[0m' : ' \x1b[33m[unsigned]\x1b[0m')
5153
+ : ' \x1b[90m[no lockfile entry]\x1b[0m';
5154
+ console.log(` \x1b[32m${s.name}\x1b[0m [${label}] ${status}${lockBadge}`);
5113
5155
  console.log(` type: ${s.type}`);
5114
5156
  console.log(` url: ${s.url}`);
5115
5157
  console.log(` trust_level: ${s.trust_level}`);
5116
5158
  console.log(` safety_policy: ${s.safety_policy}`);
5117
5159
  console.log(` checksum: ${s.checksum_required ? 'required (SHA-256 integrity)' : 'not required'}`);
5118
- console.log(` signature: ${s.signature_required ? 'required' : 'not required (v3.0.1)'}`);
5160
+ console.log(` signature: ${s.signature_required ? 'required (HMAC-SHA256)' : 'not required'}`);
5119
5161
  if (s.last_synced_at) console.log(` last_synced: ${s.last_synced_at}`);
5162
+ if (lockEntry) console.log(` lockfile: synced ${lockEntry.synced_at}, hash ${lockEntry.catalog_sha256.slice(0, 16)}...`);
5120
5163
  });
5121
5164
 
5122
5165
  console.log('\nUse \x1b[36mregistry show <name>\x1b[0m to view detailed source configuration.');
5123
5166
  console.log('Use \x1b[36mregistry status\x1b[0m to see policy states and cache health.');
5124
- console.log('Use \x1b[36mregistry verify <name>\x1b[0m to perform integrity checks.\n');
5167
+ console.log('Use \x1b[36mregistry verify <name>\x1b[0m to perform integrity checks.');
5168
+ console.log('Use \x1b[36mregistry lock\x1b[0m to inspect the provenance lockfile.\n');
5125
5169
  }
5126
5170
 
5127
5171
  function handleRegistryAdd(name, url, options) {
@@ -5286,7 +5330,6 @@ function handleRegistrySync(name, options) {
5286
5330
  process.exit(1);
5287
5331
  }
5288
5332
 
5289
- // Sync requires network - output sync initiation message
5290
5333
  const cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
5291
5334
  if (!existsSync(cacheDir)) {
5292
5335
  mkdirSync(cacheDir, { recursive: true });
@@ -5295,7 +5338,6 @@ function handleRegistrySync(name, options) {
5295
5338
  console.log(`\n🔄 \x1b[36mSyncing Registry: ${name}\x1b[0m`);
5296
5339
  console.log('==================================================');
5297
5340
 
5298
- // Attempt HTTPS download using Node built-in
5299
5341
  const url = source.url;
5300
5342
  const catalogUrl = url.endsWith('/') ? `${url}catalog.yaml` : url;
5301
5343
  const manifestUrl = catalogUrl.replace(/catalog\.yaml$/, 'manifest.json');
@@ -5304,7 +5346,6 @@ function handleRegistrySync(name, options) {
5304
5346
  const catalogDest = join(cacheDir, 'catalog.yaml');
5305
5347
  const manifestDest = join(cacheDir, 'manifest.json');
5306
5348
 
5307
- // Synchronous HTTP download using inline Node script via execFileSync
5308
5349
  const fetchUrlSync = (targetUrl) => {
5309
5350
  validateRegistryUrl(targetUrl, policy);
5310
5351
 
@@ -5345,7 +5386,6 @@ function handleRegistrySync(name, options) {
5345
5386
  console.log(` → \x1b[33mNot found (optional)\x1b[0m`);
5346
5387
  }
5347
5388
 
5348
- // Compute checksums
5349
5389
  console.log('Computing checksums...');
5350
5390
  const checksums = {
5351
5391
  'catalog.yaml': `sha256:${computeSHA256(catalogData)}`
@@ -5364,7 +5404,6 @@ function handleRegistrySync(name, options) {
5364
5404
  for (const [file, hash] of Object.entries(manifestObj.files_hashes)) {
5365
5405
  if (file === 'catalog.yaml' || file === 'manifest.json') continue;
5366
5406
 
5367
- // Check path safety inside registry cache
5368
5407
  const fileDest = join(cacheDir, file);
5369
5408
  const relativeToCache = relative(cacheDir, fileDest);
5370
5409
  if (relativeToCache.includes('..') || isAbsolute(relativeToCache)) {
@@ -5411,7 +5450,6 @@ function handleRegistrySync(name, options) {
5411
5450
  writeFileSync(join(cacheDir, 'checksums.json'), checksumsJson, 'utf8');
5412
5451
  console.log(` → .ai/registry-cache/${name}/checksums.json ... OK`);
5413
5452
 
5414
- // Verify checksum against manifest if available
5415
5453
  if (policy.require_checksum && manifestData) {
5416
5454
  try {
5417
5455
  const manifest = JSON.parse(manifestData);
@@ -5430,12 +5468,106 @@ function handleRegistrySync(name, options) {
5430
5468
  } catch (e) {}
5431
5469
  }
5432
5470
 
5433
- // Update last_synced_at
5434
- source.last_synced_at = new Date().toISOString();
5471
+ const syncedAt = new Date().toISOString();
5472
+ source.last_synced_at = syncedAt;
5435
5473
  source.pinned_commit_or_hash = computeSHA256(catalogData);
5436
5474
  saveRegistrySources(sources);
5437
5475
 
5438
- // Count plugins
5476
+ // --- Provenance lockfile + signature verification ---
5477
+ const catalogHash = computeSHA256(catalogData);
5478
+ const manifestHash = manifestData ? computeSHA256(manifestData) : null;
5479
+ const projectDir = options.target || process.cwd();
5480
+
5481
+ let signingKey = null;
5482
+ let signature = null;
5483
+ try {
5484
+ signingKey = loadSigningKey(projectDir);
5485
+ } catch (sigKeyErr) {
5486
+ console.log(` \x1b[33mWarning: Signing key error — ${sigKeyErr.message}\x1b[0m`);
5487
+ }
5488
+
5489
+ if (signingKey) {
5490
+ try {
5491
+ signature = signPayload(signingKey, catalogHash);
5492
+ console.log(' \x1b[32m✓ Catalog signed with project signing key (HMAC-SHA256)\x1b[0m');
5493
+ } catch (signErr) {
5494
+ console.log(` \x1b[33mWarning: Signing failed — ${signErr.message}\x1b[0m`);
5495
+ }
5496
+ } else {
5497
+ if (policy.require_signature) {
5498
+ console.error(`\x1b[31mError: policy require_signature is true but no signing key found.\x1b[0m`);
5499
+ console.error(` Generate a key with: npx multimodel-dev-os registry keygen --approved`);
5500
+ process.exit(1);
5501
+ }
5502
+ console.log(' \x1b[33m⚠ No signing key — provenance recorded without signature.\x1b[0m');
5503
+ console.log(' Generate a key with: npx multimodel-dev-os registry keygen --approved');
5504
+ }
5505
+
5506
+ // Signature Block Verification on synced data
5507
+ const trustedKeys = loadTrustedKeys(projectDir, policy);
5508
+ let verifyRes = { verified: true, status: 'unsigned' };
5509
+ let parsedManifest = null;
5510
+ if (manifestData) {
5511
+ try {
5512
+ parsedManifest = JSON.parse(manifestData);
5513
+ verifyRes = verifySignatureBlock({
5514
+ manifest: parsedManifest,
5515
+ trustedKeys,
5516
+ policy,
5517
+ hmacKey: signingKey,
5518
+ source
5519
+ });
5520
+ } catch (_e) {}
5521
+ } else {
5522
+ if (policy.require_signature || policy.allow_unsigned_remote === false) {
5523
+ verifyRes = { verified: false, error: 'Manifest missing but signature is required by policy.' };
5524
+ }
5525
+ }
5526
+
5527
+ const firstSig = parsedManifest && (parsedManifest.signature || (Array.isArray(parsedManifest.signatures) && parsedManifest.signatures[0]));
5528
+ const sigBlock = firstSig && typeof firstSig === 'object' ? firstSig : null;
5529
+
5530
+ let trustedPublisherStatus = 'unknown';
5531
+ if (sigBlock && sigBlock.key_id) {
5532
+ const tk = trustedKeys.find(k => k.key_id === sigBlock.key_id);
5533
+ if (tk) {
5534
+ trustedPublisherStatus = tk.status || 'inactive';
5535
+ }
5536
+ }
5537
+
5538
+ let trustVerdict = 'failed';
5539
+ if (verifyRes.verified) {
5540
+ if (verifyRes.status === 'verified') {
5541
+ trustVerdict = 'verified';
5542
+ } else {
5543
+ trustVerdict = 'unsigned_allowed';
5544
+ }
5545
+ }
5546
+
5547
+ const verificationErrors = verifyRes.errors || (verifyRes.error ? [verifyRes.error] : []);
5548
+ const verificationWarnings = verifyRes.warning ? [verifyRes.warning] : [];
5549
+
5550
+ const lockfile = loadRegistryLockfile(projectDir);
5551
+ updateLockfileEntry(lockfile, name, {
5552
+ url: source.url,
5553
+ synced_at: options.synced_at || syncedAt, // Allow override for test determinism
5554
+ catalog_sha256: catalogHash,
5555
+ manifest_sha256: manifestHash,
5556
+ signature,
5557
+ signature_alg: 'hmac-sha256',
5558
+ public_signature_status: verifyRes.status || 'unsigned',
5559
+ public_signature_algorithm: sigBlock ? sigBlock.algorithm : null,
5560
+ public_signature_key_id: sigBlock ? sigBlock.key_id : null,
5561
+ trusted_publisher_status: trustedPublisherStatus,
5562
+ trust_store_path: policy.trusted_keys_file || '.ai/registries/trusted-keys.yaml',
5563
+ trust_verdict: trustVerdict,
5564
+ lockfile_verdict: 'verified',
5565
+ verification_errors: verificationErrors,
5566
+ verification_warnings: verificationWarnings
5567
+ });
5568
+ saveRegistryLockfile(projectDir, lockfile);
5569
+ console.log(` \x1b[32m✓ Provenance lockfile updated: .ai/registry-lock.json\x1b[0m`);
5570
+
5439
5571
  let pluginCount = 0;
5440
5572
  try {
5441
5573
  const catParsed = parseYaml(catalogData);
@@ -5446,10 +5578,12 @@ function handleRegistrySync(name, options) {
5446
5578
  console.log(` Cache location: .ai/registry-cache/${name}/`);
5447
5579
  console.log(` Plugins cached: ${pluginCount} entries`);
5448
5580
  console.log(` Checksum status: VERIFIED (SHA256)`);
5449
- console.log(` Last synced: ${source.last_synced_at}`);
5581
+ console.log(` Provenance: ${signature ? 'SIGNED (HMAC-SHA256)' : 'Unsigned (no signing key)'}`);
5582
+ console.log(` Last synced: ${syncedAt}`);
5450
5583
  console.log(`\nNext steps:`);
5451
5584
  console.log(` • Browse: npx multimodel-dev-os catalog list --source remote:${name}`);
5452
5585
  console.log(` • Verify: npx multimodel-dev-os registry verify ${name}`);
5586
+ console.log(` • Lock: npx multimodel-dev-os registry lock`);
5453
5587
  console.log(` • Install: npx multimodel-dev-os catalog install <slug> --approved\n`);
5454
5588
  } catch (e) {
5455
5589
  console.error(`\n\x1b[31mSync failed: ${e.message}\x1b[0m`);
@@ -5466,20 +5600,53 @@ function handleRegistryStatus(options) {
5466
5600
  const policy = loadRegistryPolicy(options.target);
5467
5601
 
5468
5602
  if (options.json) {
5469
- console.log(JSON.stringify({ sources, policy: { allow_remote_registries: policy.allow_remote_registries, require_checksum: policy.require_checksum } }, null, 2));
5603
+ console.log(JSON.stringify({ sources, policy }, null, 2));
5470
5604
  return;
5471
5605
  }
5472
5606
 
5607
+ const projectDir = options.target || process.cwd();
5608
+ let signingKeyStatus = '\x1b[90mnot configured\x1b[0m';
5609
+ try {
5610
+ const sk = loadSigningKey(projectDir);
5611
+ signingKeyStatus = sk ? `\x1b[32mconfigured\x1b[0m (${getSigningKeyPath(projectDir)})` : '\x1b[90mnot configured\x1b[0m';
5612
+ } catch (e) {
5613
+ signingKeyStatus = `\x1b[31merror: ${e.message}\x1b[0m`;
5614
+ }
5615
+
5616
+ const lockfile = loadRegistryLockfile(projectDir);
5617
+ const lockfileEntryCount = Object.keys(lockfile.entries).length;
5618
+ const lockfilePath = getLockfilePath(projectDir);
5619
+ const lockfileStatus = existsSync(lockfilePath)
5620
+ ? `\x1b[32mpresent\x1b[0m (${lockfileEntryCount} entr${lockfileEntryCount === 1 ? 'y' : 'ies'})`
5621
+ : '\x1b[90mnot present\x1b[0m';
5622
+
5473
5623
  console.log(`\n📊 \x1b[36mRegistry Status [v${version}]\x1b[0m`);
5474
5624
  console.log('==================================================');
5475
5625
  console.log(`\x1b[33mPolicy State:\x1b[0m`);
5476
- console.log(` allow_remote_registries: \x1b[${policy.allow_remote_registries ? '32mtrue' : '33mfalse'}\x1b[0m (Disabled by default)`);
5477
- console.log(` require_checksum: ${policy.require_checksum ? '\x1b[32mtrue\x1b[0m (SHA256 integrity enforced)' : '\x1b[33mfalse\x1b[0m'}`);
5478
- console.log(` require_signature: ${policy.require_signature ? '\x1b[32mtrue\x1b[0m' : '\x1b[90mfalse (not enforced in v3.0)\x1b[0m'}`);
5479
- console.log(` allow_untrusted_install: ${policy.allow_untrusted_install ? '\x1b[33mtrue\x1b[0m' : '\x1b[32mfalse\x1b[0m (secured)'}`);
5480
- console.log(` max_plugin_files: ${policy.max_plugin_files}`);
5481
- console.log(` max_plugin_size_kb: ${policy.max_plugin_size_kb}KB`);
5482
- console.log(` max_registry_cache_size: ${policy.max_registry_cache_size_kb}KB`);
5626
+ console.log(` allow_remote_registries: \x1b[${policy.allow_remote_registries ? '32mtrue' : '33mfalse'}\x1b[0m (Disabled by default)`);
5627
+ console.log(` require_checksum: ${policy.require_checksum ? '\x1b[32mtrue\x1b[0m (SHA256 integrity enforced)' : '\x1b[33mfalse\x1b[0m'}`);
5628
+ console.log(` require_signature: ${policy.require_signature ? '\x1b[32mtrue\x1b[0m (HMAC-SHA256 enforced)' : '\x1b[90mfalse\x1b[0m'}`);
5629
+ console.log(` require_lockfile_on_verify: ${policy.require_lockfile_on_verify ? '\x1b[32mtrue\x1b[0m' : '\x1b[90mfalse\x1b[0m'}`);
5630
+ console.log(` allow_untrusted_install: ${policy.allow_untrusted_install ? '\x1b[33mtrue\x1b[0m' : '\x1b[32mfalse\x1b[0m (secured)'}`);
5631
+ console.log(` allow_unsigned_local: ${policy.allow_unsigned_local ? '\x1b[32mtrue\x1b[0m' : '\x1b[33mfalse\x1b[0m'}`);
5632
+ console.log(` allow_unsigned_bundled: ${policy.allow_unsigned_bundled ? '\x1b[32mtrue\x1b[0m' : '\x1b[33mfalse\x1b[0m'}`);
5633
+ console.log(` allow_unsigned_remote: ${policy.allow_unsigned_remote ? '\x1b[32mtrue\x1b[0m' : '\x1b[33mfalse\x1b[0m'}`);
5634
+ console.log(` require_trusted_publisher: ${policy.require_trusted_publisher ? '\x1b[32mtrue\x1b[0m' : '\x1b[90mfalse\x1b[0m'}`);
5635
+ console.log(` provenance_required: ${policy.provenance_required ? '\x1b[32mtrue\x1b[0m' : '\x1b[90mfalse\x1b[0m'}`);
5636
+ console.log(` trusted_keys_file: \x1b[36m${policy.trusted_keys_file}\x1b[0m`);
5637
+ console.log(` allowed_signature_algs: \x1b[36m${(policy.allowed_signature_algorithms || []).join(', ')}\x1b[0m`);
5638
+ console.log(` max_plugin_files: ${policy.max_plugin_files}`);
5639
+ console.log(` max_plugin_size_kb: ${policy.max_plugin_size_kb}KB`);
5640
+ console.log(` max_registry_cache_size: ${policy.max_registry_cache_size_kb}KB`);
5641
+ console.log(`\n\x1b[33mSigning & Provenance:\x1b[0m`);
5642
+ console.log(` Signing key: ${signingKeyStatus}`);
5643
+ console.log(` Lockfile: ${lockfileStatus}`);
5644
+ if (lockfileEntryCount > 0) {
5645
+ Object.entries(lockfile.entries).forEach(([rName, entry]) => {
5646
+ const sigBadge = entry.signature ? '\x1b[32m[signed]\x1b[0m' : '\x1b[33m[unsigned]\x1b[0m';
5647
+ console.log(` ${rName}: ${sigBadge} synced ${entry.synced_at || 'unknown'}`);
5648
+ });
5649
+ }
5483
5650
 
5484
5651
  console.log(`\n\x1b[33mSources:\x1b[0m`);
5485
5652
  sources.forEach(s => {
@@ -5506,88 +5673,360 @@ function handleRegistryVerify(name, options) {
5506
5673
  console.log(`\n🔍 \x1b[36mVerifying Registry: ${name}\x1b[0m`);
5507
5674
  console.log('==================================================');
5508
5675
 
5509
- if (name === 'bundled') {
5510
- const catalogPath = join(sourceRoot, '.ai', 'plugins', 'catalog.yaml');
5511
- if (!existsSync(catalogPath)) {
5512
- console.error('\x1b[31mError: Bundled catalog.yaml not found.\x1b[0m');
5513
- process.exit(1);
5514
- }
5515
- const content = readFileSync(catalogPath, 'utf8');
5516
- const hash = computeSHA256(content);
5517
- console.log(` Verification Type: Local verification (no network required)`);
5518
- console.log(` File: .ai/plugins/catalog.yaml`);
5519
- console.log(` SHA256 Checksum: ${hash}`);
5520
- console.log(` Status: \x1b[32m✓ Present and readable (Integrity Verified)\x1b[0m`);
5676
+ const projectDir = options.target || process.cwd();
5677
+ const policy = loadRegistryPolicy(projectDir);
5678
+ const sources = loadRegistrySources();
5679
+ const source = sources.find(s => s.name === name);
5521
5680
 
5522
- // Verify it parses
5523
- try {
5524
- const parsed = parseYaml(content);
5525
- const pluginCount = ((parsed.catalog || {}).plugins || []).length;
5526
- console.log(` Plugins: ${pluginCount} entries parsed successfully`);
5527
- console.log(`\n\x1b[32m✔ Bundled registry verification passed. (Offline & Secure)\x1b[0m\n`);
5528
- } catch (e) {
5529
- console.error(`\n\x1b[31m✗ Bundled registry verification failed: ${e.message}\x1b[0m\n`);
5530
- process.exit(1);
5531
- }
5532
- return;
5681
+ let isBundled = name === 'bundled';
5682
+ let isLocal = source ? source.type === 'local' : false;
5683
+ let isRemote = source ? source.type === 'remote' : false;
5684
+ let url = source ? source.url : (isBundled ? '.ai/plugins/catalog.yaml' : null);
5685
+
5686
+ if (!source && !isBundled) {
5687
+ console.error(`\x1b[31mError: Registry '${name}' is not configured.\x1b[0m`);
5688
+ process.exit(1);
5533
5689
  }
5534
5690
 
5535
- // Verify remote cache
5536
- const sources = loadRegistrySources();
5537
- const source = sources.find(s => s.name === name);
5538
- if (source && source.type !== 'local') {
5539
- const policy = loadRegistryPolicy(options.target || process.cwd());
5691
+ let urlValidationStatus = 'N/A';
5692
+ if (isRemote) {
5540
5693
  try {
5541
- validateRegistryUrl(source.url, policy);
5694
+ validateRegistryUrl(url, policy);
5695
+ urlValidationStatus = '\x1b[32m✓ Valid HTTPS\x1b[0m';
5542
5696
  } catch (err) {
5543
- console.error(`\x1b[31mError: Registry '${name}' has an invalid URL: ${err.message}\x1b[0m`);
5544
- process.exit(1);
5697
+ urlValidationStatus = `\x1b[31m✗ Invalid: ${err.message}\x1b[0m`;
5545
5698
  }
5699
+ } else {
5700
+ urlValidationStatus = '\x1b[32m✓ Valid Local Path\x1b[0m';
5546
5701
  }
5547
5702
 
5548
- const cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
5549
- if (!existsSync(cacheDir)) {
5703
+ let cacheDir;
5704
+ if (isBundled) {
5705
+ cacheDir = join(sourceRoot, '.ai', 'plugins');
5706
+ } else {
5707
+ cacheDir = join(sourceRoot, '.ai', 'registry-cache', name);
5708
+ }
5709
+
5710
+ const catalogDest = join(cacheDir, 'catalog.yaml');
5711
+ const manifestDest = join(cacheDir, 'manifest.json');
5712
+ const checksumPath = join(cacheDir, 'checksums.json');
5713
+
5714
+ if (!isBundled && !existsSync(cacheDir)) {
5550
5715
  console.error(`\x1b[31mError: No cache found for registry '${name}'. Run registry sync first.\x1b[0m`);
5551
5716
  process.exit(1);
5552
5717
  }
5553
5718
 
5554
- const checksumPath = join(cacheDir, 'checksums.json');
5555
- if (!existsSync(checksumPath)) {
5556
- console.error(`\x1b[31mError: No checksums.json found in cache for '${name}'.\x1b[0m`);
5719
+ if (isBundled && !existsSync(catalogDest)) {
5720
+ console.error(`\x1b[31mError: Bundled catalog.yaml not found.\x1b[0m`);
5557
5721
  process.exit(1);
5558
5722
  }
5559
5723
 
5560
- try {
5561
- const checksums = JSON.parse(readFileSync(checksumPath, 'utf8'));
5562
- let allPassed = true;
5563
-
5564
- Object.entries(checksums).forEach(([file, expectedHash]) => {
5565
- const filePath = join(cacheDir, file);
5566
- if (!existsSync(filePath)) {
5567
- console.log(` \x1b[31m✗ ${file}: MISSING\x1b[0m`);
5568
- allPassed = false;
5569
- return;
5724
+ let catalogContent = '';
5725
+ let catalogHash = 'N/A';
5726
+ if (existsSync(catalogDest)) {
5727
+ catalogContent = readFileSync(catalogDest, 'utf8');
5728
+ catalogHash = computeSHA256(catalogContent);
5729
+ }
5730
+
5731
+ let manifestObj = null;
5732
+ let manifestHash = 'N/A';
5733
+ if (existsSync(manifestDest)) {
5734
+ const manifestData = readFileSync(manifestDest, 'utf8');
5735
+ manifestHash = computeSHA256(manifestData);
5736
+ try {
5737
+ manifestObj = JSON.parse(manifestData);
5738
+ } catch (e) {
5739
+ console.warn(`\x1b[33mWarning: Failed to parse manifest.json: ${e.message}\x1b[0m`);
5740
+ }
5741
+ }
5742
+
5743
+ let integrityVerified = true;
5744
+ if (!isBundled) {
5745
+ if (!existsSync(checksumPath)) {
5746
+ console.log(` \x1b[33m⚠ Checksums: Missing checksums.json in cache\x1b[0m`);
5747
+ integrityVerified = false;
5748
+ } else {
5749
+ try {
5750
+ const checksums = JSON.parse(readFileSync(checksumPath, 'utf8'));
5751
+ Object.entries(checksums).forEach(([file, expectedHash]) => {
5752
+ const filePath = join(cacheDir, file);
5753
+ if (!existsSync(filePath)) {
5754
+ console.log(` \x1b[31m✗ File missing in cache: ${file}\x1b[0m`);
5755
+ integrityVerified = false;
5756
+ return;
5757
+ }
5758
+ const content = readFileSync(filePath, 'utf8');
5759
+ const actualHash = `sha256:${computeSHA256(content)}`;
5760
+ if (actualHash === expectedHash) {
5761
+ console.log(` \x1b[32m✓ ${file}: VERIFIED (Integrity check matched via SHA-256)\x1b[0m`);
5762
+ } else {
5763
+ console.log(` \x1b[31m✗ ${file}: MISMATCH\x1b[0m`);
5764
+ console.log(` Expected: ${expectedHash}`);
5765
+ console.log(` Actual: ${actualHash}`);
5766
+ integrityVerified = false;
5767
+ }
5768
+ });
5769
+ } catch (e) {
5770
+ console.log(` \x1b[31m✗ Integrity: Failed to verify checksums: ${e.message}\x1b[0m`);
5771
+ integrityVerified = false;
5570
5772
  }
5571
- const content = readFileSync(filePath, 'utf8');
5572
- const actualHash = `sha256:${computeSHA256(content)}`;
5573
- if (actualHash === expectedHash) {
5574
- console.log(` \x1b[32m✓ ${file}: VERIFIED (Integrity check matched via SHA-256)\x1b[0m`);
5773
+ }
5774
+ }
5775
+
5776
+ const lockfile = loadRegistryLockfile(projectDir);
5777
+ const lockEntry = lockfile.entries[name];
5778
+ let lockfileStatus = 'N/A';
5779
+ let provenanceStatus = 'N/A';
5780
+ let lockfileVerdict = 'N/A';
5781
+
5782
+ if (!isBundled) {
5783
+ const lockfilePath = getLockfilePath(projectDir);
5784
+ lockfileStatus = existsSync(lockfilePath) ? `\x1b[32mpresent\x1b[0m` : `\x1b[33mmissing\x1b[0m`;
5785
+
5786
+ if (!lockEntry) {
5787
+ if (policy.require_lockfile_on_verify) {
5788
+ provenanceStatus = `\x1b[31m✗ Failed (require_lockfile_on_verify is true but entry missing)\x1b[0m`;
5789
+ lockfileVerdict = 'Failed';
5790
+ } else {
5791
+ provenanceStatus = `\x1b[33m⚠ Missing provenance entry (no sync lock)\x1b[0m`;
5792
+ lockfileVerdict = 'Missing';
5793
+ }
5794
+ } else {
5795
+ let isProvMatch = true;
5796
+ if (catalogHash !== lockEntry.catalog_sha256) {
5797
+ isProvMatch = false;
5798
+ console.log(` \x1b[31m✗ Lockfile catalog hash mismatch: Expected ${lockEntry.catalog_sha256}, got ${catalogHash}\x1b[0m`);
5799
+ }
5800
+ if (manifestHash !== 'N/A' && lockEntry.manifest_sha256 && manifestHash !== lockEntry.manifest_sha256) {
5801
+ isProvMatch = false;
5802
+ console.log(` \x1b[31m✗ Lockfile manifest hash mismatch: Expected ${lockEntry.manifest_sha256}, got ${manifestHash}\x1b[0m`);
5803
+ }
5804
+ if (isProvMatch) {
5805
+ provenanceStatus = `\x1b[32m✓ Matched lockfile entry\x1b[0m`;
5806
+ lockfileVerdict = 'Verified';
5575
5807
  } else {
5576
- console.log(` \x1b[31m✗ ${file}: MISMATCH\x1b[0m`);
5577
- console.log(` Expected: ${expectedHash}`);
5578
- console.log(` Actual: ${actualHash}`);
5579
- allPassed = false;
5808
+ provenanceStatus = `\x1b[31m✗ Tampering detected: hashes do not match lockfile\x1b[0m`;
5809
+ lockfileVerdict = 'Tampered';
5580
5810
  }
5811
+ }
5812
+ } else {
5813
+ lockfileStatus = 'N/A (Bundled)';
5814
+ provenanceStatus = '\x1b[32m✓ Implicit Trust\x1b[0m';
5815
+ lockfileVerdict = 'Verified';
5816
+ }
5817
+
5818
+ const trustedKeys = loadTrustedKeys(projectDir, policy);
5819
+ let hmacKey = null;
5820
+ try {
5821
+ hmacKey = loadSigningKey(projectDir);
5822
+ } catch (_e) {}
5823
+
5824
+ let signatureAlgorithm = 'None';
5825
+ let signatureKeyId = 'None';
5826
+ let trustedPublisherStatus = 'N/A';
5827
+ let signatureValidity = 'N/A';
5828
+ let signatureResult = { verified: true, status: 'unsigned' };
5829
+
5830
+ if (manifestObj) {
5831
+ signatureResult = verifySignatureBlock({
5832
+ manifest: manifestObj,
5833
+ trustedKeys,
5834
+ policy,
5835
+ hmacKey,
5836
+ source: source || { name: 'bundled', type: 'local' }
5581
5837
  });
5582
5838
 
5583
- if (allPassed) {
5584
- console.log(`\n\x1b[32m✔ Registry '${name}' verification passed.\x1b[0m\n`);
5839
+ const signatureBlocks = [];
5840
+ if (manifestObj.signature && typeof manifestObj.signature === 'object') {
5841
+ signatureBlocks.push(manifestObj.signature);
5842
+ }
5843
+ if (Array.isArray(manifestObj.signatures)) {
5844
+ signatureBlocks.push(...manifestObj.signatures);
5845
+ }
5846
+
5847
+ if (signatureBlocks.length > 0) {
5848
+ const firstSig = signatureBlocks[0];
5849
+ signatureAlgorithm = firstSig.algorithm || 'unknown';
5850
+ signatureKeyId = firstSig.key_id || 'unknown';
5851
+
5852
+ const tk = trustedKeys.find(k => k.key_id === signatureKeyId);
5853
+ if (tk) {
5854
+ trustedPublisherStatus = tk.status === 'active' ? `\x1b[32m✓ Trusted (${tk.name})\x1b[0m` : `\x1b[31m✗ ${tk.status} (${tk.name})\x1b[0m`;
5855
+ } else {
5856
+ trustedPublisherStatus = `\x1b[33m⚠ Unknown key_id (Not in trust store)\x1b[0m`;
5857
+ }
5858
+
5859
+ if (signatureResult.verified) {
5860
+ signatureValidity = `\x1b[32m✓ Valid Signature\x1b[0m`;
5861
+ } else {
5862
+ const errorMsg = signatureResult.errors ? signatureResult.errors.join(', ') : (signatureResult.error || 'signature verification failed');
5863
+ signatureValidity = `\x1b[31m✗ Invalid Signature (${errorMsg})\x1b[0m`;
5864
+ }
5585
5865
  } else {
5586
- console.error(`\n\x1b[31m✗ Registry '${name}' verification failed. Re-sync recommended.\x1b[0m\n`);
5587
- process.exit(1);
5866
+ if (policy.require_signature || (isRemote && policy.allow_unsigned_remote === false)) {
5867
+ signatureValidity = `\x1b[31m✗ Missing Signature (Enforced by policy)\x1b[0m`;
5868
+ } else {
5869
+ signatureValidity = `\x1b[90mUnsigned\x1b[0m`;
5870
+ }
5871
+ }
5872
+ } else {
5873
+ if (!isBundled && (policy.require_signature || (isRemote && policy.allow_unsigned_remote === false))) {
5874
+ signatureResult = { verified: false, error: 'Manifest missing but signature is required by policy.' };
5875
+ signatureValidity = `\x1b[31m✗ Manifest missing (Enforced by policy)\x1b[0m`;
5876
+ } else {
5877
+ signatureValidity = `\x1b[90mUnsigned (No manifest)\x1b[0m`;
5878
+ }
5879
+ }
5880
+
5881
+ console.log(` Source Type: ${isBundled ? 'bundled' : source.type}`);
5882
+ console.log(` Source URL/Path: ${url}`);
5883
+ console.log(` URL Validation: ${urlValidationStatus}`);
5884
+ console.log(` Manifest SHA256: ${manifestHash}`);
5885
+ console.log(` Catalog SHA256: ${catalogHash}`);
5886
+ console.log(` Lockfile Status: ${lockfileStatus}`);
5887
+ console.log(` Provenance Status: ${provenanceStatus}`);
5888
+ console.log(` Signature Alg: ${signatureAlgorithm}`);
5889
+ console.log(` Signature Key ID: ${signatureKeyId}`);
5890
+ console.log(` Trusted Publisher: ${trustedPublisherStatus}`);
5891
+ console.log(` Signature Validity: ${signatureValidity}`);
5892
+
5893
+ let finalVerdict = '✗ Failed';
5894
+ let passed = true;
5895
+
5896
+ if (!integrityVerified) passed = false;
5897
+ if (!isBundled && lockfileVerdict === 'Failed') passed = false;
5898
+ if (!isBundled && lockfileVerdict === 'Tampered') passed = false;
5899
+ if (!signatureResult.verified) passed = false;
5900
+
5901
+ if (passed) {
5902
+ if (signatureResult.status === 'verified') {
5903
+ finalVerdict = `\x1b[32m✓ Verified (Signature matches trusted key)\x1b[0m`;
5904
+ } else if (isBundled || isLocal) {
5905
+ finalVerdict = `\x1b[32m✓ Verified (Implicit local trust)\x1b[0m`;
5906
+ } else {
5907
+ finalVerdict = `\x1b[33m⚠ Unsigned (Allowed by policy)\x1b[0m`;
5588
5908
  }
5909
+ } else {
5910
+ const reason = !integrityVerified
5911
+ ? 'Integrity check failed'
5912
+ : (lockfileVerdict === 'Tampered'
5913
+ ? 'Lockfile tampering detected'
5914
+ : (signatureResult.error || (signatureResult.errors && signatureResult.errors.join(', ')) || 'Signature verification failed'));
5915
+ finalVerdict = `\x1b[31m✗ Failed (${reason})\x1b[0m`;
5916
+ }
5917
+
5918
+ console.log(` Final Trust: ${finalVerdict}`);
5919
+ console.log('==================================================');
5920
+
5921
+ try {
5922
+ const parsed = parseYaml(catalogContent);
5923
+ const pluginCount = ((parsed.catalog || {}).plugins || []).length;
5924
+ console.log(` Plugins Parsed: ${pluginCount} entries`);
5589
5925
  } catch (e) {
5590
- console.error(`\x1b[31mError: Failed to read checksums: ${e.message}\x1b[0m`);
5926
+ console.error(`\x1b[31m✗ Catalog parsing failed: ${e.message}\x1b[0m`);
5927
+ process.exit(1);
5928
+ }
5929
+
5930
+ const verdict = createTrustVerdict({
5931
+ source: name,
5932
+ source_type: isBundled ? 'bundled' : (source ? source.type : 'remote'),
5933
+ manifest_hash_status: manifestHash !== 'N/A' ? (lockEntry && manifestHash === lockEntry.manifest_sha256 ? 'verified' : (manifestObj ? 'unverified' : 'missing')) : 'N/A',
5934
+ catalog_hash_status: catalogHash !== 'N/A' ? (lockEntry && catalogHash === lockEntry.catalog_sha256 ? 'verified' : 'unverified') : 'N/A',
5935
+ lockfile_status: isBundled ? 'N/A' : (lockEntry ? 'present' : 'missing'),
5936
+ provenance_status: isBundled ? 'N/A' : (lockEntry ? (lockfileVerdict === 'Verified' ? 'matched' : (lockfileVerdict === 'Tampered' ? 'mismatch' : 'missing')) : 'N/A'),
5937
+ signature_status: signatureResult.status || 'unsigned',
5938
+ trusted_publisher_status: signatureResult.status === 'verified' ? 'trusted' : 'N/A',
5939
+ errors: signatureResult.errors || (signatureResult.error ? [signatureResult.error] : []),
5940
+ warnings: signatureResult.warning ? [signatureResult.warning] : [],
5941
+ final_status: passed ? (signatureResult.status === 'verified' ? 'trusted' : (isBundled || isLocal ? 'trusted' : 'warning')) : 'untrusted'
5942
+ });
5943
+
5944
+ if (!isBundled && lockEntry) {
5945
+ lockEntry.trust_verdict = passed ? (signatureResult.status === 'verified' ? 'verified' : 'unsigned_allowed') : 'failed';
5946
+ lockEntry.lockfile_verdict = lockfileVerdict.toLowerCase();
5947
+ lockEntry.verification_errors = verdict.errors;
5948
+ lockEntry.verification_warnings = verdict.warnings;
5949
+ lockEntry.verdict = verdict;
5950
+ saveRegistryLockfile(projectDir, lockfile);
5951
+ }
5952
+
5953
+ if (passed) {
5954
+ console.log(`\n\x1b[32m✔ Registry '${name}' verification passed.\x1b[0m\n`);
5955
+ } else {
5956
+ console.error(`\n\x1b[31m✗ Registry '${name}' verification failed.\x1b[0m\n`);
5957
+ process.exit(1);
5958
+ }
5959
+ }
5960
+
5961
+ function handleRegistryTrustList(options) {
5962
+ const projectDir = options.target || process.cwd();
5963
+ const policy = loadRegistryPolicy(projectDir);
5964
+ const keys = loadTrustedKeys(projectDir, policy);
5965
+
5966
+ console.log(`\n🔑 \x1b[36mRegistry Trust Store — Trusted Keys\x1b[0m`);
5967
+ console.log('==================================================');
5968
+ console.log(`Trust Store Path: \x1b[36m${policy.trusted_keys_file || '.ai/registries/trusted-keys.yaml'}\x1b[0m`);
5969
+ console.log(`Total Keys: ${keys.length}\n`);
5970
+
5971
+ if (keys.length === 0) {
5972
+ console.log(' No trusted keys configured.');
5973
+ } else {
5974
+ keys.forEach(k => {
5975
+ const statusBadge = k.status === 'active' ? '\x1b[32m● active\x1b[0m' : `\x1b[31m○ ${k.status}\x1b[0m`;
5976
+ console.log(` * \x1b[33m${k.key_id}\x1b[0m [${statusBadge}]`);
5977
+ console.log(` Publisher: ${k.name}`);
5978
+ console.log(` Algorithm: ${k.algorithm}`);
5979
+ console.log(` Scopes: ${(k.scopes || []).join(', ')}`);
5980
+ });
5981
+ }
5982
+ console.log('');
5983
+ }
5984
+
5985
+ function handleRegistryTrustShow(keyId, options) {
5986
+ const projectDir = options.target || process.cwd();
5987
+ const policy = loadRegistryPolicy(projectDir);
5988
+ const keys = loadTrustedKeys(projectDir, policy);
5989
+ const k = keys.find(key => key.key_id === keyId);
5990
+
5991
+ if (!k) {
5992
+ console.error(`\x1b[31mError: Trusted key '${keyId}' not found in the trust store.\x1b[0m`);
5993
+ process.exit(1);
5994
+ }
5995
+
5996
+ console.log(`\n🔑 \x1b[36mTrusted Key: ${keyId}\x1b[0m`);
5997
+ console.log('==================================================');
5998
+ console.log(`\x1b[33mKey ID:\x1b[0m ${k.key_id}`);
5999
+ console.log(`\x1b[33mPublisher:\x1b[0m ${k.name}`);
6000
+ console.log(`\x1b[33mAlgorithm:\x1b[0m ${k.algorithm}`);
6001
+ console.log(`\x1b[33mStatus:\x1b[0m ${k.status === 'active' ? '\x1b[32mactive\x1b[0m' : `\x1b[31m${k.status}\x1b[0m`}`);
6002
+ console.log(`\x1b[33mScopes:\x1b[0m ${(k.scopes || []).join(', ')}`);
6003
+ console.log(`\x1b[33mPublic Key:\x1b[0m\n${k.public_key.trim()}`);
6004
+ console.log('');
6005
+ }
6006
+
6007
+ function handleRegistryTrustVerify(options) {
6008
+ const projectDir = options.target || process.cwd();
6009
+ const policy = loadRegistryPolicy(projectDir);
6010
+ const keys = loadTrustedKeys(projectDir, policy);
6011
+
6012
+ console.log(`\n🔑 \x1b[36mVerifying Trust Store Integrity...\x1b[0m`);
6013
+ console.log('==================================================');
6014
+
6015
+ let passed = true;
6016
+ keys.forEach(k => {
6017
+ try {
6018
+ normalizePublicKey(k.public_key);
6019
+ console.log(` \x1b[32m✓\x1b[0m Key '${k.key_id}' public key format is valid.`);
6020
+ } catch (e) {
6021
+ console.log(` \x1b[31m✗\x1b[0m Key '${k.key_id}' public key format error: ${e.message}`);
6022
+ passed = false;
6023
+ }
6024
+ });
6025
+
6026
+ if (passed) {
6027
+ console.log(`\n\x1b[32m✔ Trust store verification passed.\x1b[0m\n`);
6028
+ } else {
6029
+ console.error(`\n\x1b[31m✗ Trust store verification failed.\x1b[0m\n`);
5591
6030
  process.exit(1);
5592
6031
  }
5593
6032
  }
@@ -5716,3 +6155,109 @@ function handleRegistryCacheClear(options) {
5716
6155
  console.log(` Directories processed: ${cleared}`);
5717
6156
  console.log(` Cache root: .ai/registry-cache/\n`);
5718
6157
  }
6158
+
6159
+ // --- Registry Signing Handlers ---
6160
+
6161
+ function handleRegistryKeygen(options) {
6162
+ const projectDir = options.target || process.cwd();
6163
+ const keyPath = getSigningKeyPath(projectDir);
6164
+
6165
+ console.log(`\n🔑 \x1b[36mRegistry Signing Key Generator\x1b[0m`);
6166
+ console.log('==================================================');
6167
+
6168
+ if (!options.approved) {
6169
+ console.error('\x1b[31mError: Signing key generation requires explicit approval. Pass the --approved flag.\x1b[0m');
6170
+ console.log(`\n\x1b[33mPlanned Action:\x1b[0m Generate a 32-byte random HMAC-SHA256 signing key.`);
6171
+ console.log(` Destination: ${keyPath}`);
6172
+ console.log(` Mode: 0o600 (owner read/write only)`);
6173
+ console.log(`\n\x1b[33mSecurity Notes:\x1b[0m`);
6174
+ console.log(` • Add .ai/registry-signing-key to your .gitignore`);
6175
+ console.log(` • Share the key securely with trusted team members for co-verification`);
6176
+ console.log(` • The key is used for HMAC-SHA256 signing of catalog checksums only`);
6177
+ console.log(`\nTo generate, run:`);
6178
+ console.log(` \x1b[36mnpx multimodel-dev-os registry keygen --approved\x1b[0m\n`);
6179
+ process.exit(1);
6180
+ }
6181
+
6182
+ // Check existing key
6183
+ let existingKey = null;
6184
+ try {
6185
+ existingKey = loadSigningKey(projectDir);
6186
+ } catch (_e) {}
6187
+
6188
+ if (existingKey && !options.force) {
6189
+ console.error(`\x1b[31mError: A signing key already exists at: ${keyPath}\x1b[0m`);
6190
+ console.log(`\nTo overwrite, run with --force:`);
6191
+ console.log(` \x1b[36mnpx multimodel-dev-os registry keygen --approved --force\x1b[0m`);
6192
+ console.log(`\n\x1b[33mWarning:\x1b[0m Overwriting will invalidate all existing signatures in the lockfile.\n`);
6193
+ process.exit(1);
6194
+ }
6195
+
6196
+ const newKey = generateSigningKey();
6197
+ saveSigningKey(projectDir, newKey);
6198
+
6199
+ console.log(`\n\x1b[32m✔ Signing key generated successfully!\x1b[0m`);
6200
+ console.log(` Location: ${keyPath}`);
6201
+ console.log(` Mode: 0o600 (restricted permissions)`);
6202
+ console.log(`\n\x1b[33mNext steps:\x1b[0m`);
6203
+ console.log(` 1. Add to .gitignore: echo '.ai/registry-signing-key' >> .gitignore`);
6204
+ console.log(` 2. Re-sync registries to generate signed lockfile entries:`);
6205
+ console.log(` npx multimodel-dev-os registry sync <name> --approved`);
6206
+ console.log(` 3. Verify signed provenance:`);
6207
+ console.log(` npx multimodel-dev-os registry verify <name>\n`);
6208
+ }
6209
+
6210
+ function handleRegistryLock(options) {
6211
+ const projectDir = options.target || process.cwd();
6212
+ const lockfilePath = getLockfilePath(projectDir);
6213
+
6214
+ console.log(`\n🔒 \x1b[36mRegistry Provenance Lockfile\x1b[0m`);
6215
+ console.log('==================================================');
6216
+
6217
+ if (!existsSync(lockfilePath)) {
6218
+ console.log(` \x1b[90mNo lockfile found at: ${lockfilePath}\x1b[0m`);
6219
+ console.log(` Sync a remote registry to create it:`);
6220
+ console.log(` npx multimodel-dev-os registry sync <name> --approved\n`);
6221
+ return;
6222
+ }
6223
+
6224
+ const lockfile = loadRegistryLockfile(projectDir);
6225
+ const entries = Object.entries(lockfile.entries);
6226
+
6227
+ if (options.json) {
6228
+ console.log(JSON.stringify(lockfile, null, 2));
6229
+ return;
6230
+ }
6231
+
6232
+ console.log(` Lockfile version: ${lockfile.lockfile_version}`);
6233
+ console.log(` Generated at: ${lockfile.generated_at}`);
6234
+ console.log(` Path: ${lockfilePath}`);
6235
+ console.log(` Entries: ${entries.length}\n`);
6236
+
6237
+ if (entries.length === 0) {
6238
+ console.log(` \x1b[90mNo registry entries recorded yet.\x1b[0m`);
6239
+ console.log(` Sync a remote registry to populate:\n npx multimodel-dev-os registry sync <name> --approved\n`);
6240
+ return;
6241
+ }
6242
+
6243
+ entries.forEach(([name, entry]) => {
6244
+ const sigBadge = entry.signature
6245
+ ? `\x1b[32m[SIGNED — HMAC-SHA256]\x1b[0m`
6246
+ : `\x1b[33m[UNSIGNED]\x1b[0m`;
6247
+ console.log(` \x1b[32m${name}\x1b[0m ${sigBadge}`);
6248
+ console.log(` URL: ${entry.url}`);
6249
+ console.log(` Synced at: ${entry.synced_at}`);
6250
+ console.log(` Catalog SHA-256: ${entry.catalog_sha256}`);
6251
+ if (entry.manifest_sha256) {
6252
+ console.log(` Manifest SHA256: ${entry.manifest_sha256}`);
6253
+ }
6254
+ if (entry.signature) {
6255
+ console.log(` Signature: ${entry.signature.slice(0, 24)}...`);
6256
+ console.log(` Sig algorithm: ${entry.signature_alg}`);
6257
+ }
6258
+ console.log('');
6259
+ });
6260
+
6261
+ console.log('Use \x1b[36mregistry verify <name>\x1b[0m to re-verify cached files against the lockfile.');
6262
+ console.log('Use \x1b[36mregistry keygen --approved\x1b[0m to generate a signing key for HMAC signatures.\n');
6263
+ }