multimodel-dev-os 3.1.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 (102) 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 +2 -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 +17 -0
  17. package/docs/public/llms-full.txt +5 -1
  18. package/docs/public/llms.txt +6 -1
  19. package/docs/public/sitemap.xml +15 -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 +6 -5
  26. package/docs/security-threat-model.md +96 -0
  27. package/docs/testing.md +25 -2
  28. package/docs/trusted-registries.md +1 -1
  29. package/docs/v3-roadmap.md +17 -6
  30. package/docs/v3.5.0-readiness.md +46 -0
  31. package/package.json +5 -2
  32. package/scripts/build-cli.js +45 -3
  33. package/scripts/check-build-fresh.js +52 -0
  34. package/scripts/install.ps1 +1 -1
  35. package/scripts/install.sh +1 -1
  36. package/scripts/verify.js +327 -14
  37. package/scripts/verify.sh +10 -0
  38. package/src/catalog/loader.js +117 -0
  39. package/src/cli/args.js +118 -0
  40. package/src/cli/help.js +60 -0
  41. package/src/cli/main.js +6263 -0
  42. package/src/core/globals.js +52 -0
  43. package/src/core/hashes.js +15 -0
  44. package/src/core/policy.js +44 -0
  45. package/src/core/security.js +61 -0
  46. package/src/core/yaml.js +136 -0
  47. package/src/plugin/manifest.js +95 -0
  48. package/src/registry/provenance.js +114 -0
  49. package/src/registry/signing.js +392 -0
  50. package/src/registry/sources.js +40 -0
  51. package/src/registry/trust-store.js +41 -0
  52. package/src/registry/validation.js +45 -0
  53. package/src/registry/verdict.js +51 -0
  54. package/tests/README.md +37 -0
  55. package/tests/fixtures/README.md +22 -0
  56. package/tests/fixtures/custom-template-example/README.md +10 -0
  57. package/tests/fixtures/proposals/approved-append-line.md +28 -0
  58. package/tests/fixtures/proposals/approved-create-file.md +29 -0
  59. package/tests/fixtures/proposals/approved-replace-text.md +30 -0
  60. package/tests/fixtures/proposals/existing-create-file-no-overwrite.md +29 -0
  61. package/tests/fixtures/proposals/no-operations.md +18 -0
  62. package/tests/fixtures/proposals/path-traversal.md +29 -0
  63. package/tests/fixtures/proposals/pending-proposal.md +29 -0
  64. package/tests/fixtures/proposals/protected-path.md +29 -0
  65. package/tests/fixtures/proposals/replace-multiple-without-allow.md +30 -0
  66. package/tests/fixtures/registry-overrides/README.md +20 -0
  67. package/tests/fixtures/signed-registries/README.md +4 -0
  68. package/tests/fixtures/signed-registries/revoked-key/catalog.yaml +8 -0
  69. package/tests/fixtures/signed-registries/revoked-key/expected-verdict.json +7 -0
  70. package/tests/fixtures/signed-registries/revoked-key/registry-manifest.yaml +14 -0
  71. package/tests/fixtures/signed-registries/tampered-manifest/catalog.yaml +8 -0
  72. package/tests/fixtures/signed-registries/tampered-manifest/expected-verdict.json +7 -0
  73. package/tests/fixtures/signed-registries/tampered-manifest/registry-manifest.yaml +14 -0
  74. package/tests/fixtures/signed-registries/trusted-keys.yaml +23 -0
  75. package/tests/fixtures/signed-registries/unsigned-remote-required/catalog.yaml +8 -0
  76. package/tests/fixtures/signed-registries/unsigned-remote-required/expected-verdict.json +7 -0
  77. package/tests/fixtures/signed-registries/unsigned-remote-required/registry-manifest.yaml +9 -0
  78. package/tests/fixtures/signed-registries/unsupported-algorithm/catalog.yaml +8 -0
  79. package/tests/fixtures/signed-registries/unsupported-algorithm/expected-verdict.json +7 -0
  80. package/tests/fixtures/signed-registries/unsupported-algorithm/registry-manifest.yaml +14 -0
  81. package/tests/fixtures/signed-registries/valid-signed-registry/catalog.yaml +8 -0
  82. package/tests/fixtures/signed-registries/valid-signed-registry/expected-verdict.json +7 -0
  83. package/tests/fixtures/signed-registries/valid-signed-registry/registry-manifest.yaml +14 -0
  84. package/tests/fixtures/signed-registries/wrong-key/catalog.yaml +8 -0
  85. package/tests/fixtures/signed-registries/wrong-key/expected-verdict.json +7 -0
  86. package/tests/fixtures/signed-registries/wrong-key/registry-manifest.yaml +14 -0
  87. package/tests/smoke/README.md +37 -0
  88. package/tests/smoke/cli-smoke.md +49 -0
  89. package/tests/unit/build-output.test.js +40 -0
  90. package/tests/unit/catalog-loader.test.js +44 -0
  91. package/tests/unit/path-safety.test.js +62 -0
  92. package/tests/unit/plugin-manifest.test.js +94 -0
  93. package/tests/unit/prepublish-guard.test.js +35 -0
  94. package/tests/unit/registry-e2e-signature-fixtures.test.js +288 -0
  95. package/tests/unit/registry-policy.test.js +52 -0
  96. package/tests/unit/registry-provenance.test.js +185 -0
  97. package/tests/unit/registry-public-signing.test.js +109 -0
  98. package/tests/unit/registry-signature-policy.test.js +100 -0
  99. package/tests/unit/registry-signing.test.js +193 -0
  100. package/tests/unit/registry-trust-store.test.js +133 -0
  101. package/tests/unit/registry-url-validation.test.js +64 -0
  102. package/tests/unit/yaml.test.js +92 -0
package/scripts/verify.js CHANGED
@@ -189,6 +189,24 @@ checkFile('scripts/pack-template.sh');
189
189
  checkFile('scripts/prepublish-guard.js');
190
190
  checkFile('bin/multimodel-dev-os.js');
191
191
 
192
+ // --- Modular Source Files ---
193
+ console.log('\nModular Source Files:');
194
+ checkFile('src/cli/main.js');
195
+ checkFile('src/cli/args.js');
196
+ checkFile('src/cli/help.js');
197
+ checkFile('src/core/yaml.js');
198
+ checkFile('src/core/hashes.js');
199
+ checkFile('src/core/policy.js');
200
+ checkFile('src/core/security.js');
201
+ checkFile('src/core/globals.js');
202
+ checkFile('src/registry/validation.js');
203
+ checkFile('src/registry/sources.js');
204
+ checkFile('src/registry/provenance.js');
205
+ checkFile('src/registry/signing.js');
206
+ checkFile('src/registry/trust-store.js');
207
+ checkFile('src/catalog/loader.js');
208
+ checkFile('src/plugin/manifest.js');
209
+
192
210
  // --- GitHub Integration ---
193
211
  console.log('\nGitHub Workflows:');
194
212
  checkFile('.github/workflows/verify.yml');
@@ -231,6 +249,8 @@ checkFile('docs/registry-contribution.md');
231
249
  checkFile('docs/v2-migration.md');
232
250
  checkFile('docs/v2-release-checklist.md');
233
251
  checkFile('docs/package-safety.md');
252
+ checkFile('docs/registry-signing.md');
253
+ checkFile('docs/registry-trust-store.md');
234
254
 
235
255
  // --- v2.1.0 Intelligence Layer Documentation ---
236
256
  console.log('\nIntelligence Layer Documentation:');
@@ -265,6 +285,7 @@ console.log('\nJSON Schemas:');
265
285
  checkFile('.ai/schema/config.schema.json');
266
286
  checkFile('.ai/schema/template.schema.json');
267
287
  checkFile('.ai/schema/adapter.schema.json');
288
+ checkFile('.ai/schema/trusted-keys.schema.json');
268
289
 
269
290
  // --- v2.1.0 Intelligence Layer (Schemas, Policies, Registries) ---
270
291
  console.log('\nIntelligence Layer Schemas:');
@@ -287,8 +308,25 @@ console.log('\nIntelligence Layer Registries:');
287
308
  checkFile('.ai/registries/capabilities.yaml');
288
309
  checkFile('.ai/registries/tools.yaml');
289
310
  checkFile('.ai/registries/workflows.yaml');
290
-
291
- // --- Test Blueprints ---
311
+ checkFile('.ai/registries/trusted-keys.yaml');
312
+
313
+ // --- Unit Tests ---
314
+ console.log('\nUnit Tests:');
315
+ checkFile('tests/unit/yaml.test.js');
316
+ checkFile('tests/unit/registry-url-validation.test.js');
317
+ checkFile('tests/unit/registry-policy.test.js');
318
+ checkFile('tests/unit/registry-provenance.test.js');
319
+ checkFile('tests/unit/registry-signing.test.js');
320
+ checkFile('tests/unit/registry-public-signing.test.js');
321
+ checkFile('tests/unit/registry-trust-store.test.js');
322
+ checkFile('tests/unit/registry-signature-policy.test.js');
323
+ checkFile('tests/unit/path-safety.test.js');
324
+ checkFile('tests/unit/plugin-manifest.test.js');
325
+ checkFile('tests/unit/catalog-loader.test.js');
326
+ checkFile('tests/unit/build-output.test.js');
327
+ checkFile('tests/unit/prepublish-guard.test.js');
328
+
329
+ // --- Test Manuals & Fixtures ---
292
330
  console.log('\nTest Manuals:');
293
331
  checkFile('tests/README.md');
294
332
  checkFile('tests/fixtures/README.md');
@@ -549,7 +587,7 @@ try {
549
587
  }
550
588
  }
551
589
 
552
- // Test 2: Allows version 3.1.0 with MMDO_ALLOW_PUBLISH=true
590
+ // Test 2: Allows version 3.5.0 with MMDO_ALLOW_PUBLISH=true
553
591
  try {
554
592
  const output = execSync('node scripts/prepublish-guard.js', {
555
593
  cwd: projectRoot,
@@ -557,7 +595,7 @@ try {
557
595
  encoding: 'utf8'
558
596
  });
559
597
  if (output.includes('Prepublish guard passed')) {
560
- console.log(` ${GREEN}✓${NC} prepublish guard allows version 3.1.0 when MMDO_ALLOW_PUBLISH=true`);
598
+ console.log(` ${GREEN}✓${NC} prepublish guard allows version 3.5.0 when MMDO_ALLOW_PUBLISH=true`);
561
599
  pass++;
562
600
  } else {
563
601
  console.error(` ${RED}✗${NC} prepublish guard passed but stdout missing success indicator`);
@@ -565,7 +603,7 @@ try {
565
603
  }
566
604
  } catch (err) {
567
605
  const errText = err.stderr ? err.stderr.toString() : '';
568
- console.error(` ${RED}✗${NC} prepublish guard blocked version 3.1.0: ${errText || err.message}`);
606
+ console.error(` ${RED}✗${NC} prepublish guard blocked version 3.5.0: ${errText || err.message}`);
569
607
  fail++;
570
608
  }
571
609
 
@@ -579,12 +617,12 @@ try {
579
617
  pass++;
580
618
  }
581
619
 
582
- // Test 4: Package.json version is exactly 3.1.0
583
- if (expectedVersion === '3.1.0') {
584
- console.log(` ${GREEN}✓${NC} package.json version is exactly 3.1.0`);
620
+ // Test 4: Package.json version is exactly 3.5.0
621
+ if (expectedVersion === '3.5.0') {
622
+ console.log(` ${GREEN}✓${NC} package.json version is exactly 3.5.0`);
585
623
  pass++;
586
624
  } else {
587
- console.error(` ${RED}✗${NC} package.json version is not 3.1.0 (found ${expectedVersion})`);
625
+ console.error(` ${RED}✗${NC} package.json version is not 3.5.0 (found ${expectedVersion})`);
588
626
  fail++;
589
627
  }
590
628
  } catch (e) {
@@ -592,6 +630,52 @@ try {
592
630
  fail++;
593
631
  }
594
632
 
633
+ // --- Post-build Generated CLI Checks ---
634
+ console.log('\nPost-build Generated CLI Checks:');
635
+ try {
636
+ // 0. Check build freshness
637
+ try {
638
+ execSync('node scripts/check-build-fresh.js', { cwd: projectRoot, stdio: 'ignore' });
639
+ console.log(` ${GREEN}✓${NC} generated bin matches current source layout`);
640
+ pass++;
641
+ } catch (err) {
642
+ console.error(` ${RED}✗${NC} generated bin is stale! Run 'npm run build' and commit bin/multimodel-dev-os.js`);
643
+ fail++;
644
+ }
645
+
646
+ const buildPath = join(projectRoot, 'bin', 'multimodel-dev-os.js');
647
+ const binContent = readFileSync(buildPath, 'utf8');
648
+
649
+ const totalShebangs = (binContent.match(/#!/g) || []).length;
650
+ if (binContent.startsWith('#!/usr/bin/env node') && totalShebangs === 1) {
651
+ console.log(` ${GREEN}✓${NC} generated bin has exactly one shebang at the top`);
652
+ pass++;
653
+ } else {
654
+ console.error(` ${RED}✗${NC} generated bin has invalid shebang layout (count: ${totalShebangs})`);
655
+ fail++;
656
+ }
657
+
658
+ if (binContent.includes('// Generated from src/. Do not edit directly.')) {
659
+ console.log(` ${GREEN}✓${NC} generated bin has warning header`);
660
+ pass++;
661
+ } else {
662
+ console.error(` ${RED}✗${NC} generated bin is missing the warning header`);
663
+ fail++;
664
+ }
665
+
666
+ const hasUnsafeSync = binContent.includes("mod.get('${targetUrl}'") || (binContent.includes('execSync(`node -e "') && binContent.includes('${targetUrl}'));
667
+ if (!hasUnsafeSync && binContent.includes('execFileSync(process.execPath')) {
668
+ console.log(` ${GREEN}✓${NC} generated bin is free of unsafe URL interpolation and uses execFileSync`);
669
+ pass++;
670
+ } else {
671
+ console.error(` ${RED}✗${NC} generated bin fails safety scan (unsafe interpolation found)`);
672
+ fail++;
673
+ }
674
+ } catch (e) {
675
+ console.error(` ${RED}✗${NC} post-build generated CLI checks failed: ${e.message}`);
676
+ fail++;
677
+ }
678
+
595
679
  // --- v2.8.0 / v2.8.1 Dashboard & Plugin Tests ---
596
680
  console.log('\nRunning TUI Dashboard & Plugin Pre-Flight Tests...');
597
681
 
@@ -921,22 +1005,77 @@ try {
921
1005
  fail++;
922
1006
  }
923
1007
 
924
- // Verify npm pack dry-run shows current version dynamically
1008
+ // Verify npm pack dry-run shows current version dynamically and has clean hygiene
925
1009
  try {
926
- const packOutput = execSync('npm pack --dry-run', { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
927
- if (packOutput.includes(`multimodel-dev-os@${expectedVersion}`) || packOutput.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || packOutput.includes(`version: ${expectedVersion}`)) {
1010
+ const packOutput = execSync('npm pack --dry-run 2>&1', { cwd: projectRoot, encoding: 'utf8' });
1011
+ const combinedOutput = packOutput;
1012
+
1013
+ const hasVersion = combinedOutput.includes(`multimodel-dev-os@${expectedVersion}`) || combinedOutput.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || combinedOutput.includes(`version: ${expectedVersion}`);
1014
+ if (hasVersion) {
928
1015
  console.log(` ${GREEN}✓${NC} npm pack --dry-run reports version ${expectedVersion}`);
929
1016
  pass++;
930
1017
  } else {
931
- console.error(` ${RED}✗${NC} npm pack --dry-run did not report ${expectedVersion} in stdout`);
1018
+ console.error(` ${RED}✗${NC} npm pack --dry-run did not report ${expectedVersion} in output`);
1019
+ fail++;
1020
+ }
1021
+
1022
+ // Hygiene checks
1023
+ const lines = combinedOutput.split('\n');
1024
+ const files = lines
1025
+ .filter(l => l.includes('npm notice') && !l.includes('Tarball Details') && !l.includes('Tarball Filename') && !l.includes('package size:') && !l.includes('unpacked size:') && !l.includes('shasum:') && !l.includes('integrity:') && !l.includes('total files:'))
1026
+ .map(l => {
1027
+ const match = l.match(/npm notice\s+\d+(\.\d+)?[a-zA-Z]+\s+(.+)$/);
1028
+ return match ? match[2].trim() : '';
1029
+ })
1030
+ .filter(f => f !== '');
1031
+
1032
+ const hasSrc = files.some(f => f.startsWith('src/'));
1033
+ const hasTests = files.some(f => f.startsWith('tests/'));
1034
+
1035
+ if (hasSrc && hasTests) {
1036
+ console.log(` ${GREEN}✓${NC} npm pack includes 'src/' and 'tests/' directories`);
1037
+ pass++;
1038
+ } else {
1039
+ console.error(` ${RED}✗${NC} npm pack is missing 'src/' or 'tests/' directory`);
1040
+ fail++;
1041
+ }
1042
+
1043
+ const hasBlacklisted = files.some(f => f.includes('.npmrc') || f.includes('.env') || f.includes('node_modules') || f.endsWith('.tgz') || f.includes('coverage/'));
1044
+ if (!hasBlacklisted) {
1045
+ console.log(` ${GREEN}✓${NC} npm pack excludes sensitive and temporary files (.npmrc, .env, node_modules, .tgz, coverage)`);
1046
+ pass++;
1047
+ } else {
1048
+ console.error(` ${RED}✗${NC} npm pack contains blacklisted files!`);
932
1049
  fail++;
933
1050
  }
934
1051
  } catch (e) {
935
1052
  const stdErrOut = e.stderr ? e.stderr.toString() : '';
936
1053
  const stdOutOut = e.stdout ? e.stdout.toString() : '';
937
- if (stdErrOut.includes(`multimodel-dev-os@${expectedVersion}`) || stdErrOut.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || stdOutOut.includes(`multimodel-dev-os-${expectedVersion}.tgz`)) {
1054
+ const combined = stdErrOut + '\n' + stdOutOut;
1055
+
1056
+ const hasVersion = combined.includes(`multimodel-dev-os@${expectedVersion}`) || combined.includes(`multimodel-dev-os-${expectedVersion}.tgz`) || combined.includes(`version: ${expectedVersion}`);
1057
+ if (hasVersion) {
938
1058
  console.log(` ${GREEN}✓${NC} npm pack --dry-run reports version ${expectedVersion}`);
939
1059
  pass++;
1060
+
1061
+ const hasSrc = combined.includes('src/') || combined.includes('src\\');
1062
+ const hasTests = combined.includes('tests/') || combined.includes('tests\\');
1063
+ if (hasSrc && hasTests) {
1064
+ console.log(` ${GREEN}✓${NC} npm pack includes 'src/' and 'tests/' directories`);
1065
+ pass++;
1066
+ } else {
1067
+ console.error(` ${RED}✗${NC} npm pack is missing 'src/' or 'tests/' directory`);
1068
+ fail++;
1069
+ }
1070
+
1071
+ const hasBlacklisted = combined.includes('.npmrc') || combined.includes('.env') || combined.includes('node_modules') || combined.includes('.tgz') || combined.includes('coverage/');
1072
+ if (!hasBlacklisted) {
1073
+ console.log(` ${GREEN}✓${NC} npm pack excludes sensitive and temporary files`);
1074
+ pass++;
1075
+ } else {
1076
+ console.error(` ${RED}✗${NC} npm pack contains blacklisted files!`);
1077
+ fail++;
1078
+ }
940
1079
  } else {
941
1080
  console.error(` ${RED}✗${NC} npm pack --dry-run failed or did not report ${expectedVersion}: ${e.message}`);
942
1081
  fail++;
@@ -1271,6 +1410,180 @@ const checkExamplesHygiene = (dir) => {
1271
1410
  };
1272
1411
  checkExamplesHygiene(join(projectRoot, 'examples'));
1273
1412
 
1413
+ // --- Registry Signing & Provenance Checks ---
1414
+ console.log('\nRegistry Signing & Provenance Checks:');
1415
+
1416
+ // Check .gitignore contains registry-signing-key
1417
+ try {
1418
+ const gitignoreContent = readFileSync(join(projectRoot, '.gitignore'), 'utf8');
1419
+ if (gitignoreContent.includes('registry-signing-key')) {
1420
+ console.log(` ${GREEN}✓${NC} .gitignore includes registry-signing-key pattern`);
1421
+ pass++;
1422
+ } else {
1423
+ console.error(` ${RED}✗${NC} .gitignore is missing the registry-signing-key entry (secrets must be gitignored)`);
1424
+ fail++;
1425
+ }
1426
+ } catch (e) {
1427
+ console.error(` ${RED}✗${NC} Failed to read .gitignore: ${e.message}`);
1428
+ fail++;
1429
+ }
1430
+
1431
+ // Check provenance.js exports the expected API surface
1432
+ try {
1433
+ const provenanceSrc = readFileSync(join(projectRoot, 'src', 'registry', 'provenance.js'), 'utf8');
1434
+ const hasLoadLockfile = provenanceSrc.includes('export function loadRegistryLockfile');
1435
+ const hasSaveLockfile = provenanceSrc.includes('export function saveRegistryLockfile');
1436
+ const hasUpdateEntry = provenanceSrc.includes('export function updateLockfileEntry');
1437
+ const hasGetPath = provenanceSrc.includes('export function getLockfilePath');
1438
+ if (hasLoadLockfile && hasSaveLockfile && hasUpdateEntry && hasGetPath) {
1439
+ console.log(` ${GREEN}✓${NC} src/registry/provenance.js exports complete API (load/save/update/getPath)`);
1440
+ pass++;
1441
+ } else {
1442
+ console.error(` ${RED}✗${NC} src/registry/provenance.js is missing expected exports`);
1443
+ fail++;
1444
+ }
1445
+ } catch (e) {
1446
+ console.error(` ${RED}✗${NC} Failed to check provenance.js: ${e.message}`);
1447
+ fail++;
1448
+ }
1449
+
1450
+ // Check signing.js exports the expected API surface
1451
+ try {
1452
+ const signingSrc = readFileSync(join(projectRoot, 'src', 'registry', 'signing.js'), 'utf8');
1453
+ const hasLoadKey = signingSrc.includes('export function loadSigningKey');
1454
+ const hasGenKey = signingSrc.includes('export function generateSigningKey');
1455
+ const hasSaveKey = signingSrc.includes('export function saveSigningKey');
1456
+ const hasSign = signingSrc.includes('export function signPayload');
1457
+ const hasVerify = signingSrc.includes('export function verifySignature');
1458
+ const hasTimingSafe = signingSrc.includes('timingSafeEqual');
1459
+ const hasEdKeygen = signingSrc.includes('export function generateEd25519KeyPair');
1460
+ const hasEdSign = signingSrc.includes('export function signEd25519Payload');
1461
+ const hasEdVerify = signingSrc.includes('export function verifyEd25519Payload');
1462
+ const hasSigBlockVerify = signingSrc.includes('export function verifySignatureBlock');
1463
+
1464
+ if (hasLoadKey && hasGenKey && hasSaveKey && hasSign && hasVerify && hasTimingSafe && hasEdKeygen && hasEdSign && hasEdVerify && hasSigBlockVerify) {
1465
+ console.log(` ${GREEN}✓${NC} src/registry/signing.js exports complete API (HMAC + Ed25519)`);
1466
+ pass++;
1467
+ } else {
1468
+ console.error(` ${RED}✗${NC} src/registry/signing.js is missing expected exports`);
1469
+ fail++;
1470
+ }
1471
+ } catch (e) {
1472
+ console.error(` ${RED}✗${NC} Failed to check signing.js: ${e.message}`);
1473
+ fail++;
1474
+ }
1475
+
1476
+ // Check trust-store.js exports expected API surface
1477
+ try {
1478
+ const trustSrc = readFileSync(join(projectRoot, 'src', 'registry', 'trust-store.js'), 'utf8');
1479
+ const hasLoadTrustedKeys = trustSrc.includes('export function loadTrustedKeys');
1480
+ if (hasLoadTrustedKeys) {
1481
+ console.log(` ${GREEN}✓${NC} src/registry/trust-store.js exports loadTrustedKeys`);
1482
+ pass++;
1483
+ } else {
1484
+ console.error(` ${RED}✗${NC} src/registry/trust-store.js is missing expected exports`);
1485
+ fail++;
1486
+ }
1487
+ } catch (e) {
1488
+ console.error(` ${RED}✗${NC} Failed to check trust-store.js: ${e.message}`);
1489
+ fail++;
1490
+ }
1491
+
1492
+ // Check main.js imports the new modules
1493
+ try {
1494
+ const mainSrc = readFileSync(join(projectRoot, 'src', 'cli', 'main.js'), 'utf8');
1495
+ const hasProvenanceImport = mainSrc.includes("from '../registry/provenance.js'");
1496
+ const hasSigningImport = mainSrc.includes("from '../registry/signing.js'");
1497
+ const hasTrustImport = mainSrc.includes("from '../registry/trust-store.js'");
1498
+ const hasKeygenHandler = mainSrc.includes('handleRegistryKeygen');
1499
+ const hasLockHandler = mainSrc.includes('handleRegistryLock');
1500
+ const hasTrustHandler = mainSrc.includes('handleRegistryTrustList');
1501
+ if (hasProvenanceImport && hasSigningImport && hasTrustImport && hasKeygenHandler && hasLockHandler && hasTrustHandler) {
1502
+ console.log(` ${GREEN}✓${NC} src/cli/main.js imports provenance/signing/trust-store and registers handlers`);
1503
+ pass++;
1504
+ } else {
1505
+ console.error(` ${RED}✗${NC} src/cli/main.js is missing required imports or handlers`);
1506
+ fail++;
1507
+ }
1508
+ } catch (e) {
1509
+ console.error(` ${RED}✗${NC} Failed to check main.js integrations: ${e.message}`);
1510
+ fail++;
1511
+ }
1512
+
1513
+ // Check that policy.js has the new fields in defaults
1514
+ try {
1515
+ const policySrc = readFileSync(join(projectRoot, 'src', 'core', 'policy.js'), 'utf8');
1516
+ const hasLockfileField = policySrc.includes('require_lockfile_on_verify');
1517
+ const hasUnsignedLocal = policySrc.includes('allow_unsigned_local');
1518
+ const hasUnsignedBundled = policySrc.includes('allow_unsigned_bundled');
1519
+ const hasUnsignedRemote = policySrc.includes('allow_unsigned_remote');
1520
+ const hasTrustedKeysFile = policySrc.includes('trusted_keys_file');
1521
+ const hasAllowedAlgs = policySrc.includes('allowed_signature_algorithms');
1522
+ const hasRequireTrustedPublisher = policySrc.includes('require_trusted_publisher');
1523
+ const hasProvenanceRequired = policySrc.includes('provenance_required');
1524
+
1525
+ if (hasLockfileField && hasUnsignedLocal && hasUnsignedBundled && hasUnsignedRemote && hasTrustedKeysFile && hasAllowedAlgs && hasRequireTrustedPublisher && hasProvenanceRequired) {
1526
+ console.log(` ${GREEN}✓${NC} src/core/policy.js includes all Sprint 2 policy defaults`);
1527
+ pass++;
1528
+ } else {
1529
+ console.error(` ${RED}✗${NC} src/core/policy.js is missing required policy defaults`);
1530
+ fail++;
1531
+ }
1532
+ } catch (e) {
1533
+ console.error(` ${RED}✗${NC} Failed to check policy.js: ${e.message}`);
1534
+ fail++;
1535
+ }
1536
+
1537
+ // --- v3.5.0 Sprint 3 E2E Fixtures & Threat Model Checks ---
1538
+ console.log('\nSprint 3 Signed Registry E2E & Readiness Checks:');
1539
+ checkFile('src/registry/verdict.js');
1540
+ checkFile('tests/unit/registry-e2e-signature-fixtures.test.js');
1541
+ checkFile('docs/security-threat-model.md');
1542
+ checkFile('docs/v3.5.0-readiness.md');
1543
+
1544
+ // Verify that the trusted-keys.yaml in the E2E fixtures directory exists
1545
+ const e2eKeysPath = 'tests/fixtures/signed-registries/trusted-keys.yaml';
1546
+ if (checkFile(e2eKeysPath)) {
1547
+ const e2eKeysContent = readFileSync(join(projectRoot, e2eKeysPath), 'utf8');
1548
+ if (e2eKeysContent.includes('test-key-valid') && e2eKeysContent.includes('test-key-revoked')) {
1549
+ console.log(` ${GREEN}✓${NC} ${e2eKeysPath} is populated with test fixtures and marked for testing`);
1550
+ pass++;
1551
+ } else {
1552
+ console.error(` ${RED}✗${NC} ${e2eKeysPath} is missing expected test keys`);
1553
+ fail++;
1554
+ }
1555
+ }
1556
+
1557
+ // Verify that the threat model document has a standard threat modeling structure
1558
+ try {
1559
+ const threatModelContent = readFileSync(join(projectRoot, 'docs/security-threat-model.md'), 'utf8');
1560
+ if (threatModelContent.includes('Threat Model') && (threatModelContent.includes('STRIDE') || threatModelContent.includes('stride'))) {
1561
+ console.log(` ${GREEN}✓${NC} docs/security-threat-model.md structure verified`);
1562
+ pass++;
1563
+ } else {
1564
+ console.error(` ${RED}✗${NC} docs/security-threat-model.md is missing standard threat modeling structure`);
1565
+ fail++;
1566
+ }
1567
+ } catch (e) {
1568
+ console.error(` ${RED}✗${NC} Failed to verify threat model document: ${e.message}`);
1569
+ fail++;
1570
+ }
1571
+
1572
+ // Verify that no private keys are committed in main directories (like .ai/)
1573
+ try {
1574
+ const rootKeyFile = '.ai/registry-signing-key';
1575
+ if (existsSync(join(projectRoot, rootKeyFile))) {
1576
+ console.error(` ${RED}✗${NC} Private signing key ${rootKeyFile} should not be committed!`);
1577
+ fail++;
1578
+ } else {
1579
+ console.log(` ${GREEN}✓${NC} No private registry-signing-key found in codebase root`);
1580
+ pass++;
1581
+ }
1582
+ } catch (e) {
1583
+ console.error(` ${RED}✗${NC} Failed to check private key existence: ${e.message}`);
1584
+ fail++;
1585
+ }
1586
+
1274
1587
  console.log('\n=====================================================');
1275
1588
  const total = pass + fail + warn;
1276
1589
  console.log(` Pass: ${GREEN}${pass}${NC} Fail: ${RED}${fail}${NC} Warn: ${YELLOW}${warn}${NC} Total: ${total}`);
package/scripts/verify.sh CHANGED
@@ -192,6 +192,8 @@ check_file "docs/cli-roadmap.md"
192
192
  check_file "docs/faq.md"
193
193
  check_file "docs/testing.md"
194
194
  check_file "docs/npm-publishing.md"
195
+ check_file "docs/release-policy.md"
196
+ check_file "docs/package-safety.md"
195
197
 
196
198
  # --- CLI & Packaging Pre-Flight Tests ---
197
199
  echo ""
@@ -235,6 +237,14 @@ else
235
237
  PASS=$((PASS + 1))
236
238
  fi
237
239
 
240
+ if ! npm run check:build >/dev/null; then
241
+ echo -e " ${RED}✗${NC} Generated CLI is stale. Run npm run build."
242
+ FAIL=$((FAIL + 1))
243
+ else
244
+ echo -e " ${GREEN}✓${NC} Generated CLI is fresh"
245
+ PASS=$((PASS + 1))
246
+ fi
247
+
238
248
  if ! node bin/multimodel-dev-os.js init --dry-run --force >/dev/null; then
239
249
  echo -e " ${RED}✗${NC} node bin/multimodel-dev-os.js init --dry-run failed"
240
250
  FAIL=$((FAIL + 1))
@@ -0,0 +1,117 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { sourceRoot } from '../core/globals.js';
4
+ import { parseYaml } from '../core/yaml.js';
5
+ import { loadRegistrySources } from '../registry/sources.js';
6
+ import { loadRegistryPolicy } from '../core/policy.js';
7
+ import { validateRegistryUrl } from '../registry/validation.js';
8
+
9
+ export function loadCatalog(options = {}) {
10
+ let catalog;
11
+ if (options.allSources) {
12
+ catalog = loadAllCatalogs(options);
13
+ } else if (options.source) {
14
+ catalog = loadCatalogFromSource(options.source, options);
15
+ } else {
16
+ const path = join(sourceRoot, '.ai', 'plugins', 'catalog.yaml');
17
+ try {
18
+ if (existsSync(path)) {
19
+ const reg = parseYaml(readFileSync(path, 'utf8'));
20
+ catalog = reg.catalog || { plugins: [] };
21
+ } else {
22
+ catalog = { plugins: [] };
23
+ }
24
+ } catch (e) {
25
+ catalog = { plugins: [] };
26
+ }
27
+ (catalog.plugins || []).forEach(p => { p._source = 'bundled'; });
28
+ }
29
+ return catalog;
30
+ }
31
+
32
+ export function loadCatalogFromSource(source, options = {}) {
33
+ if (!source || source === 'bundled') {
34
+ return loadCatalog();
35
+ } else if (source === 'local') {
36
+ const localPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog.yaml');
37
+ try {
38
+ if (existsSync(localPath)) {
39
+ const reg = parseYaml(readFileSync(localPath, 'utf8'));
40
+ const catalog = reg.catalog || { plugins: [] };
41
+ (catalog.plugins || []).forEach(p => { p._source = 'local'; });
42
+ return catalog;
43
+ }
44
+ } catch (e) {}
45
+ return { plugins: [] };
46
+ } else if (source.startsWith('remote:')) {
47
+ const regName = source.substring(7);
48
+ const sources = loadRegistrySources();
49
+ const src = sources.find(s => s.name === regName);
50
+ if (src && src.type !== 'local') {
51
+ const policy = loadRegistryPolicy(options.target || process.cwd());
52
+ try {
53
+ validateRegistryUrl(src.url, policy);
54
+ } catch (err) {
55
+ console.error(`\x1b[31mError: Registry '${regName}' has an invalid URL: ${err.message}\x1b[0m`);
56
+ process.exit(1);
57
+ }
58
+ }
59
+ const cachePath = join(sourceRoot, '.ai', 'registry-cache', regName, 'catalog.yaml');
60
+ try {
61
+ if (existsSync(cachePath)) {
62
+ const reg = parseYaml(readFileSync(cachePath, 'utf8'));
63
+ const catalog = reg.catalog || { plugins: [] };
64
+ (catalog.plugins || []).forEach(p => { p._source = `remote:${regName}`; });
65
+ return catalog;
66
+ }
67
+ } catch (e) {}
68
+ return { plugins: [] };
69
+ }
70
+ return { plugins: [] };
71
+ }
72
+
73
+ export function loadAllCatalogs(options = {}) {
74
+ const sources = loadRegistrySources();
75
+ const policy = loadRegistryPolicy(options.target || process.cwd());
76
+ const allPlugins = [];
77
+
78
+ // Always include bundled
79
+ const bundled = loadCatalog();
80
+ (bundled.plugins || []).forEach(p => { p._source = 'bundled'; allPlugins.push(p); });
81
+
82
+ // Include local workspace catalog if different from bundled
83
+ const localPath = join(options.target || process.cwd(), '.ai', 'plugins', 'catalog.yaml');
84
+ if (existsSync(localPath)) {
85
+ try {
86
+ const localCat = parseYaml(readFileSync(localPath, 'utf8'));
87
+ const localPlugins = (localCat.catalog || {}).plugins || [];
88
+ localPlugins.forEach(p => {
89
+ if (!allPlugins.some(bp => bp.slug === p.slug)) {
90
+ p._source = 'local';
91
+ allPlugins.push(p);
92
+ }
93
+ });
94
+ } catch (e) {}
95
+ }
96
+
97
+ // Include remote caches if policy allows
98
+ if (policy.allow_remote_registries) {
99
+ sources.filter(s => s.type !== 'local' && s.enabled).forEach(s => {
100
+ const cachePath = join(sourceRoot, '.ai', 'registry-cache', s.name, 'catalog.yaml');
101
+ if (existsSync(cachePath)) {
102
+ try {
103
+ const remoteCat = parseYaml(readFileSync(cachePath, 'utf8'));
104
+ const remotePlugins = (remoteCat.catalog || {}).plugins || [];
105
+ remotePlugins.forEach(p => {
106
+ if (!allPlugins.some(bp => bp.slug === p.slug)) {
107
+ p._source = `remote:${s.name}`;
108
+ allPlugins.push(p);
109
+ }
110
+ });
111
+ } catch (e) {}
112
+ }
113
+ });
114
+ }
115
+
116
+ return { plugins: allPlugins };
117
+ }
@@ -0,0 +1,118 @@
1
+ import { resolve } from 'path';
2
+
3
+ export function parseArgs(args) {
4
+ const params = {
5
+ command: null,
6
+ target: process.cwd(),
7
+ template: 'general-app',
8
+ adapters: [],
9
+ caveman: false,
10
+ dryRun: false,
11
+ force: false,
12
+ help: false,
13
+ tokens: false,
14
+ modelPreset: null,
15
+ agent: null,
16
+ stack: null,
17
+ mobile: null,
18
+ aiApp: null,
19
+ json: false,
20
+ threshold: null,
21
+ registry: null,
22
+ allRegistries: false,
23
+ release: false,
24
+ type: 'unknown',
25
+ tags: '',
26
+ files: '',
27
+ title: null,
28
+ approved: false,
29
+ intelligence: false,
30
+ onboarding: false,
31
+ listActions: false,
32
+ category: null,
33
+ source: null,
34
+ allSources: false
35
+ };
36
+
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i];
39
+ if (arg === '--target' || arg === '-t') {
40
+ params.target = resolve(args[++i]);
41
+ } else if (arg === '--template') {
42
+ params.template = args[++i];
43
+ } else if (arg === '--adapter' || arg === '-a') {
44
+ params.adapters.push(args[++i]);
45
+ } else if (arg === '--caveman') {
46
+ params.caveman = true;
47
+ } else if (arg === '--dry-run' || arg === '-d') {
48
+ params.dryRun = true;
49
+ } else if (arg === '--list-actions') {
50
+ params.listActions = true;
51
+ } else if (arg === '--force' || arg === '-f') {
52
+ params.force = true;
53
+ } else if (arg === '--help' || arg === '-h') {
54
+ params.help = true;
55
+ } else if (arg === '--tokens') {
56
+ params.tokens = true;
57
+ } else if (arg === '--all-registries') {
58
+ params.allRegistries = true;
59
+ } else if (arg === '--release') {
60
+ params.release = true;
61
+ } else if (arg === '--intelligence') {
62
+ params.intelligence = true;
63
+ } else if (arg === '--onboarding') {
64
+ params.onboarding = true;
65
+ } else if (arg === '--json') {
66
+ params.json = true;
67
+ } else if (arg === '--threshold') {
68
+ params.threshold = args[++i];
69
+ } else if (arg === '--registry') {
70
+ params.registry = args[++i];
71
+ } else if (arg === '--model-preset') {
72
+ params.modelPreset = args[++i];
73
+ } else if (arg === '--agent') {
74
+ params.agent = args[++i];
75
+ } else if (arg === '--stack') {
76
+ params.stack = args[++i];
77
+ } else if (arg === '--mobile') {
78
+ params.mobile = args[++i];
79
+ } else if (arg === '--type') {
80
+ params.type = args[++i];
81
+ } else if (arg === '--tags') {
82
+ params.tags = args[++i];
83
+ } else if (arg === '--files') {
84
+ params.files = args[++i];
85
+ } else if (arg === '--title') {
86
+ params.title = args[++i];
87
+ } else if (arg === '--approved') {
88
+ params.approved = true;
89
+ } else if (arg === '--category') {
90
+ params.category = args[++i];
91
+ } else if (arg === '--source') {
92
+ params.source = args[++i];
93
+ } else if (arg === '--all-sources') {
94
+ params.allSources = true;
95
+ } else if (!params.command && !arg.startsWith('-')) {
96
+ params.command = arg;
97
+ }
98
+ }
99
+ return params;
100
+ }
101
+
102
+ export function getPositionalArgs(args) {
103
+ const positionalArgs = [];
104
+ for (let i = 0; i < args.length; i++) {
105
+ const arg = args[i];
106
+ if (arg === '--target' || arg === '-t' || arg === '--template' || arg === '--adapter' || arg === '-a' ||
107
+ arg === '--threshold' || arg === '--registry' || arg === '--model-preset' || arg === '--agent' ||
108
+ arg === '--stack' || arg === '--mobile' || arg === '--type' || arg === '--tags' || arg === '--files' ||
109
+ arg === '--title' || arg === '--category') {
110
+ i++; // skip next arg (its value)
111
+ } else if (arg.startsWith('-')) {
112
+ // it's a flag, skip
113
+ } else {
114
+ positionalArgs.push(arg);
115
+ }
116
+ }
117
+ return positionalArgs;
118
+ }