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.
- package/.ai/policies/registry-policy.yaml +29 -1
- package/.ai/registries/trusted-keys.yaml +12 -0
- package/.ai/schema/registry-manifest.schema.json +31 -2
- package/.ai/schema/registry-policy.schema.json +37 -1
- package/.ai/schema/trusted-keys.schema.json +69 -0
- package/AGENTS.md +22 -26
- package/MEMORY.md +34 -11
- package/README.md +1 -1
- package/RUNBOOK.md +28 -36
- package/TASKS.md +15 -5
- package/bin/multimodel-dev-os.js +1366 -548
- package/docs/.vitepress/config.js +3 -1
- package/docs/architecture.md +3 -1
- package/docs/index.md +5 -5
- package/docs/npm-publishing.md +5 -5
- package/docs/package-safety.md +3 -2
- package/docs/public/llms-full.txt +5 -1
- package/docs/public/llms.txt +6 -1
- package/docs/public/sitemap.xml +10 -0
- package/docs/registry-policy.md +29 -1
- package/docs/registry-security.md +73 -6
- package/docs/registry-signing.md +70 -0
- package/docs/registry-sync.md +5 -2
- package/docs/registry-trust-store.md +66 -0
- package/docs/release-policy.md +1 -1
- package/docs/security-threat-model.md +96 -0
- package/docs/testing.md +15 -2
- package/docs/trusted-registries.md +1 -1
- package/docs/v3-roadmap.md +11 -6
- package/docs/v3.5.0-readiness.md +46 -0
- package/package.json +1 -1
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/verify.js +206 -9
- package/src/cli/help.js +1 -1
- package/src/cli/main.js +626 -81
- package/src/core/policy.js +9 -1
- package/src/registry/provenance.js +114 -0
- package/src/registry/signing.js +392 -0
- package/src/registry/trust-store.js +41 -0
- package/src/registry/verdict.js +51 -0
- package/tests/fixtures/signed-registries/README.md +4 -0
- package/tests/fixtures/signed-registries/revoked-key/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/revoked-key/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/revoked-key/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/tampered-manifest/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/tampered-manifest/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/tampered-manifest/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/trusted-keys.yaml +23 -0
- package/tests/fixtures/signed-registries/unsigned-remote-required/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/unsigned-remote-required/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/unsigned-remote-required/registry-manifest.yaml +9 -0
- package/tests/fixtures/signed-registries/unsupported-algorithm/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/unsupported-algorithm/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/unsupported-algorithm/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/valid-signed-registry/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/valid-signed-registry/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/valid-signed-registry/registry-manifest.yaml +14 -0
- package/tests/fixtures/signed-registries/wrong-key/catalog.yaml +8 -0
- package/tests/fixtures/signed-registries/wrong-key/expected-verdict.json +7 -0
- package/tests/fixtures/signed-registries/wrong-key/registry-manifest.yaml +14 -0
- package/tests/unit/registry-e2e-signature-fixtures.test.js +288 -0
- package/tests/unit/registry-policy.test.js +6 -0
- package/tests/unit/registry-provenance.test.js +185 -0
- package/tests/unit/registry-public-signing.test.js +109 -0
- package/tests/unit/registry-signature-policy.test.js +100 -0
- package/tests/unit/registry-signing.test.js +193 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
5434
|
-
source.last_synced_at =
|
|
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
|
-
//
|
|
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(`
|
|
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
|
|
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:
|
|
5477
|
-
console.log(` require_checksum:
|
|
5478
|
-
console.log(` require_signature:
|
|
5479
|
-
console.log(`
|
|
5480
|
-
console.log(`
|
|
5481
|
-
console.log(`
|
|
5482
|
-
console.log(`
|
|
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
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
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
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
}
|
|
5529
|
-
|
|
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
|
-
|
|
5536
|
-
|
|
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(
|
|
5694
|
+
validateRegistryUrl(url, policy);
|
|
5695
|
+
urlValidationStatus = '\x1b[32m✓ Valid HTTPS\x1b[0m';
|
|
5542
5696
|
} catch (err) {
|
|
5543
|
-
|
|
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
|
-
|
|
5549
|
-
if (
|
|
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
|
-
|
|
5555
|
-
|
|
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
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
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
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
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
|
-
|
|
5577
|
-
|
|
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
|
-
|
|
5584
|
-
|
|
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
|
-
|
|
5587
|
-
|
|
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[
|
|
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
|
+
}
|