opmsec 0.1.0 → 0.1.3

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 (124) hide show
  1. package/.env.example +23 -13
  2. package/README.md +256 -173
  3. package/docs/architecture/agents.mdx +77 -0
  4. package/docs/architecture/benchmarks.mdx +65 -0
  5. package/docs/architecture/overview.mdx +58 -0
  6. package/docs/architecture/scanner.mdx +53 -0
  7. package/docs/cli/audit.mdx +35 -0
  8. package/docs/cli/check.mdx +44 -0
  9. package/docs/cli/fix.mdx +49 -0
  10. package/docs/cli/info.mdx +44 -0
  11. package/docs/cli/install.mdx +71 -0
  12. package/docs/cli/push.mdx +99 -0
  13. package/docs/cli/register-agent.mdx +80 -0
  14. package/docs/cli/view.mdx +52 -0
  15. package/docs/concepts/multi-agent-consensus.mdx +58 -0
  16. package/docs/concepts/on-chain-registry.mdx +74 -0
  17. package/docs/concepts/security-model.mdx +76 -0
  18. package/docs/concepts/zk-agent-verification.mdx +82 -0
  19. package/docs/configuration.mdx +82 -0
  20. package/docs/contract/deployment.mdx +57 -0
  21. package/docs/contract/events.mdx +115 -0
  22. package/docs/contract/functions.mdx +220 -0
  23. package/docs/contract/overview.mdx +58 -0
  24. package/docs/favicon.svg +5 -0
  25. package/docs/introduction.mdx +43 -0
  26. package/docs/logo/dark.svg +5 -0
  27. package/docs/logo/light.svg +5 -0
  28. package/docs/mint.json +106 -0
  29. package/docs/quickstart.mdx +133 -0
  30. package/package.json +3 -3
  31. package/packages/cli/src/commands/author-view.tsx +9 -1
  32. package/packages/cli/src/commands/check.tsx +318 -0
  33. package/packages/cli/src/commands/fix.tsx +294 -0
  34. package/packages/cli/src/commands/install.tsx +229 -33
  35. package/packages/cli/src/commands/push.tsx +53 -22
  36. package/packages/cli/src/commands/register-agent.tsx +227 -0
  37. package/packages/cli/src/components/AgentScores.tsx +20 -6
  38. package/packages/cli/src/components/Hyperlink.tsx +30 -0
  39. package/packages/cli/src/components/ScanReport.tsx +3 -2
  40. package/packages/cli/src/index.tsx +41 -5
  41. package/packages/cli/src/services/avatar.ts +43 -6
  42. package/packages/cli/src/services/chainpatrol.ts +20 -17
  43. package/packages/cli/src/services/contract.ts +41 -8
  44. package/packages/cli/src/services/ens.ts +3 -5
  45. package/packages/cli/src/services/fileverse.ts +12 -13
  46. package/packages/cli/src/services/typosquat.ts +166 -0
  47. package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
  48. package/packages/contracts/contracts/OPMRegistry.sol +63 -0
  49. package/packages/contracts/scripts/deploy.ts +22 -3
  50. package/packages/core/src/abi.ts +221 -0
  51. package/packages/core/src/benchmarks.ts +450 -0
  52. package/packages/core/src/constants.ts +20 -0
  53. package/packages/core/src/index.ts +2 -0
  54. package/packages/core/src/model-rankings.ts +115 -0
  55. package/packages/core/src/prompt.ts +58 -0
  56. package/packages/core/src/types.ts +41 -0
  57. package/packages/core/src/utils.ts +7 -3
  58. package/packages/scanner/src/agents/base-agent.ts +13 -3
  59. package/packages/scanner/src/index.ts +5 -2
  60. package/packages/scanner/src/queue/memory-queue.ts +8 -3
  61. package/packages/scanner/src/services/benchmark-runner.ts +114 -0
  62. package/packages/scanner/src/services/contract-writer.ts +2 -3
  63. package/packages/scanner/src/services/fileverse.ts +26 -7
  64. package/packages/scanner/src/services/openrouter.ts +46 -0
  65. package/packages/scanner/src/services/report-formatter.ts +122 -3
  66. package/packages/scanner/src/services/zk-verifier.ts +118 -0
  67. package/packages/web/.next/app-build-manifest.json +15 -0
  68. package/packages/web/.next/build-manifest.json +20 -0
  69. package/packages/web/.next/package.json +1 -0
  70. package/packages/web/.next/prerender-manifest.json +11 -0
  71. package/packages/web/.next/react-loadable-manifest.json +1 -0
  72. package/packages/web/.next/routes-manifest.json +1 -0
  73. package/packages/web/.next/server/app/page.js +272 -0
  74. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
  75. package/packages/web/.next/server/app-paths-manifest.json +3 -0
  76. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  77. package/packages/web/.next/server/middleware-build-manifest.js +22 -0
  78. package/packages/web/.next/server/middleware-manifest.json +6 -0
  79. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  80. package/packages/web/.next/server/next-font-manifest.js +1 -0
  81. package/packages/web/.next/server/next-font-manifest.json +1 -0
  82. package/packages/web/.next/server/pages-manifest.json +1 -0
  83. package/packages/web/.next/server/server-reference-manifest.js +1 -0
  84. package/packages/web/.next/server/server-reference-manifest.json +5 -0
  85. package/packages/web/.next/server/vendor-chunks/@swc.js +55 -0
  86. package/packages/web/.next/server/vendor-chunks/next.js +3010 -0
  87. package/packages/web/.next/server/webpack-runtime.js +209 -0
  88. package/packages/web/.next/static/chunks/app/layout.js +39 -0
  89. package/packages/web/.next/static/chunks/app/page.js +61 -0
  90. package/packages/web/.next/static/chunks/app-pages-internals.js +182 -0
  91. package/packages/web/.next/static/chunks/main-app.js +1882 -0
  92. package/packages/web/.next/static/chunks/polyfills.js +1 -0
  93. package/packages/web/.next/static/chunks/webpack.js +1393 -0
  94. package/packages/web/.next/static/css/app/layout.css +1237 -0
  95. package/packages/web/.next/static/development/_buildManifest.js +1 -0
  96. package/packages/web/.next/static/development/_ssgManifest.js +1 -0
  97. package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
  98. package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +1 -0
  99. package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +1 -0
  100. package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +22 -0
  101. package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +22 -0
  102. package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +22 -0
  103. package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +22 -0
  104. package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +12 -0
  105. package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +12 -0
  106. package/packages/web/.next/trace +5 -0
  107. package/packages/web/.next/types/app/layout.ts +84 -0
  108. package/packages/web/.next/types/app/page.ts +84 -0
  109. package/packages/web/.next/types/cache-life.d.ts +141 -0
  110. package/packages/web/.next/types/package.json +1 -0
  111. package/packages/web/.next/types/routes.d.ts +57 -0
  112. package/packages/web/.next/types/validator.ts +61 -0
  113. package/packages/web/app/globals.css +75 -0
  114. package/packages/web/app/layout.tsx +26 -0
  115. package/packages/web/app/page.tsx +358 -0
  116. package/packages/web/bun.lock +300 -0
  117. package/packages/web/next-env.d.ts +6 -0
  118. package/packages/web/next.config.ts +5 -0
  119. package/packages/web/package.json +26 -0
  120. package/packages/web/postcss.config.mjs +8 -0
  121. package/packages/web/public/favicon.svg +5 -0
  122. package/packages/web/public/logo.svg +7 -0
  123. package/packages/web/tailwind.config.ts +48 -0
  124. package/packages/web/tsconfig.json +21 -0
@@ -67,6 +67,15 @@ function sevColor(sev: string): string {
67
67
  }
68
68
 
69
69
  export function InstallCommand({ packageName, version }: InstallCommandProps) {
70
+ if (packageName) {
71
+ return <SingleInstall packageName={packageName} version={version} />;
72
+ }
73
+ return <BulkInstall />;
74
+ }
75
+
76
+ // ─── Single package install with full security pipeline ───────────────────────
77
+
78
+ function SingleInstall({ packageName, version }: { packageName: string; version?: string }) {
70
79
  const [steps, setSteps] = useState<Steps>({
71
80
  resolve: 'pending', cve: 'pending', onchain: 'pending',
72
81
  signature: 'pending', chainpatrol: 'pending', report: 'pending',
@@ -84,26 +93,19 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
84
93
  }, []);
85
94
 
86
95
  async function run() {
87
- const packages = getTargetPackages(packageName, version);
88
- if (packages.length === 0) {
89
- setError('No packages to install');
90
- return;
91
- }
92
-
93
- const pkg = packages[0];
94
96
  const r: SecurityResult = {
95
- name: pkg.name, version: pkg.version,
96
- resolvedVersion: pkg.version, cves: [],
97
+ name: packageName, version: version || 'latest',
98
+ resolvedVersion: version || 'latest', cves: [],
97
99
  blocked: false, warning: false,
98
100
  };
99
101
 
100
102
  update('resolve', 'running');
101
- r.resolvedVersion = await resolveVersion(pkg.name, pkg.version);
103
+ r.resolvedVersion = await resolveVersion(packageName, r.version);
102
104
  setResult({ ...r });
103
105
  update('resolve', 'done');
104
106
 
105
107
  update('cve', 'running');
106
- r.cves = await queryOSV(pkg.name, r.resolvedVersion);
108
+ r.cves = await queryOSV(packageName, r.resolvedVersion);
107
109
  const cveCounts = categorizeCVEs(r.cves);
108
110
  if (cveCounts.critical > 0) {
109
111
  r.blocked = true;
@@ -116,16 +118,15 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
116
118
 
117
119
  update('onchain', 'running');
118
120
  try {
119
- const info = await getPackageInfo(pkg.name, r.resolvedVersion);
121
+ const info = await getPackageInfo(packageName, r.resolvedVersion);
120
122
  r.info = info;
121
-
122
123
  if (info.exists) {
123
124
  if (info.aggregateScore >= HIGH_RISK_THRESHOLD) {
124
125
  r.blocked = true;
125
126
  r.blockReason = (r.blockReason ? r.blockReason + '; ' : '') + `risk score ${info.aggregateScore}/100`;
126
127
  } else if (info.aggregateScore >= MEDIUM_RISK_THRESHOLD) {
127
128
  r.warning = true;
128
- r.safestVersion = await getSafestVersion(pkg.name).catch(() => undefined);
129
+ r.safestVersion = await getSafestVersion(packageName).catch(() => undefined);
129
130
  }
130
131
  }
131
132
  } catch { /* not in registry */ }
@@ -148,7 +149,7 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
148
149
 
149
150
  if (!r.info?.exists) {
150
151
  update('chainpatrol', 'running');
151
- const cp = await checkPackageWithChainPatrol(pkg.name).catch(() => null);
152
+ const cp = await checkPackageWithChainPatrol(packageName).catch(() => null);
152
153
  r.chainPatrolStatus = cp?.status;
153
154
  if (cp?.status === 'BLOCKED') {
154
155
  r.blocked = true;
@@ -174,10 +175,8 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
174
175
 
175
176
  update('install', 'running');
176
177
  try {
177
- const installTarget = packageName
178
- ? `${packageName}${version ? `@${version}` : ''}`
179
- : '';
180
- execSync(`npm install ${installTarget}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
178
+ const target = `${packageName}${version ? `@${version}` : ''}`;
179
+ execSync(`npm install ${target}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
181
180
  } catch { /* non-fatal */ }
182
181
  update('install', 'done');
183
182
  setDone(true);
@@ -326,6 +325,217 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
326
325
  );
327
326
  }
328
327
 
328
+ // ─── Bulk install: scan ALL deps from package.json ────────────────────────────
329
+
330
+ interface BulkDepResult {
331
+ name: string;
332
+ version: string;
333
+ cves: OSVVulnerability[];
334
+ cvesCritical: number;
335
+ cvesHigh: number;
336
+ onChain: boolean;
337
+ score: number | null;
338
+ blocked: boolean;
339
+ blockReason?: string;
340
+ suggestedUpgrade?: string;
341
+ }
342
+
343
+ function BulkInstall() {
344
+ const [deps, setDeps] = useState<BulkDepResult[]>([]);
345
+ const [scanning, setScanning] = useState(true);
346
+ const [error, setError] = useState<string | null>(null);
347
+ const [installStatus, setInstallStatus] = useState<StepStatus>('pending');
348
+ const [total, setTotal] = useState(0);
349
+
350
+ useEffect(() => {
351
+ runBulk().catch((err) => setError(String(err)));
352
+ }, []);
353
+
354
+ async function runBulk() {
355
+ const pkgPath = path.resolve('package.json');
356
+ if (!fs.existsSync(pkgPath)) {
357
+ setError('No package.json found');
358
+ return;
359
+ }
360
+
361
+ const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
362
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
363
+ const entries = Object.entries(allDeps) as [string, string][];
364
+ setTotal(entries.length);
365
+
366
+ if (entries.length === 0) {
367
+ setScanning(false);
368
+ return;
369
+ }
370
+
371
+ const checked: BulkDepResult[] = [];
372
+
373
+ for (const [name, verRange] of entries) {
374
+ const rawVersion = String(verRange).replace(/^[\^~]/, '');
375
+ const entry: BulkDepResult = {
376
+ name, version: rawVersion,
377
+ cves: [], cvesCritical: 0, cvesHigh: 0,
378
+ onChain: false, score: null,
379
+ blocked: false,
380
+ };
381
+
382
+ const [osvResult, infoResult] = await Promise.allSettled([
383
+ queryOSV(name, rawVersion),
384
+ getPackageInfo(name, rawVersion),
385
+ ]);
386
+
387
+ if (osvResult.status === 'fulfilled' && osvResult.value.length > 0) {
388
+ entry.cves = osvResult.value;
389
+ const counts = categorizeCVEs(osvResult.value);
390
+ entry.cvesCritical = counts.critical;
391
+ entry.cvesHigh = counts.high;
392
+
393
+ if (counts.critical > 0) {
394
+ entry.blocked = true;
395
+ entry.blockReason = `${counts.critical} CRITICAL CVE(s)`;
396
+ entry.suggestedUpgrade = getBestUpgradeVersion(osvResult.value, rawVersion) || undefined;
397
+ }
398
+ }
399
+
400
+ if (infoResult.status === 'fulfilled' && infoResult.value.exists) {
401
+ entry.onChain = true;
402
+ entry.score = infoResult.value.aggregateScore;
403
+ if (entry.score >= HIGH_RISK_THRESHOLD) {
404
+ entry.blocked = true;
405
+ entry.blockReason = (entry.blockReason ? entry.blockReason + '; ' : '') + `risk ${entry.score}/100`;
406
+ }
407
+ }
408
+
409
+ checked.push(entry);
410
+ setDeps([...checked]);
411
+ }
412
+
413
+ setScanning(false);
414
+
415
+ const blockers = checked.filter((d) => d.blocked);
416
+ if (blockers.length > 0) {
417
+ setInstallStatus('error');
418
+ setError(`Blocked: ${blockers.length} package(s) have critical vulnerabilities`);
419
+ return;
420
+ }
421
+
422
+ setInstallStatus('running');
423
+ try {
424
+ execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
425
+ } catch { /* non-fatal */ }
426
+ setInstallStatus('done');
427
+ }
428
+
429
+ const blockedDeps = deps.filter((d) => d.blocked);
430
+ const warnDeps = deps.filter((d) => !d.blocked && (d.cvesHigh > 0 || (d.score !== null && d.score >= MEDIUM_RISK_THRESHOLD)));
431
+ const safeDeps = deps.filter((d) => !d.blocked && d.cvesHigh === 0 && (d.score === null || d.score < MEDIUM_RISK_THRESHOLD));
432
+ const totalCves = deps.reduce((s, d) => s + d.cves.length, 0);
433
+
434
+ return (
435
+ <Box flexDirection="column">
436
+ <Header subtitle="install" />
437
+ <Text> </Text>
438
+
439
+ <StatusLine label={`Scanning ${total} dependencies`} status={scanning ? 'running' : 'done'}
440
+ detail={!scanning ? `${deps.length} checked` : `${deps.length}/${total}`} />
441
+
442
+ {deps.length > 0 && (
443
+ <Box flexDirection="column" marginTop={1}>
444
+ {blockedDeps.length > 0 && (
445
+ <Box flexDirection="column">
446
+ <Text color="red" bold> BLOCKED ({blockedDeps.length})</Text>
447
+ {blockedDeps.map((d) => (
448
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
449
+ <Box>
450
+ <Text color="red">✖ </Text>
451
+ <Text color="white" bold>{d.name}</Text>
452
+ <Text color="gray">@{d.version}</Text>
453
+ <Text color="red"> {d.blockReason}</Text>
454
+ </Box>
455
+ {d.cves.slice(0, 3).map((cve) => {
456
+ const sev = getOSVSeverity(cve);
457
+ return (
458
+ <Box key={cve.id} marginLeft={4}>
459
+ <Text color={sevColor(sev)} bold>{sev.padEnd(9)}</Text>
460
+ <Text color="white">{cve.id} </Text>
461
+ <Text color="gray">{cve.summary?.slice(0, 50)}</Text>
462
+ </Box>
463
+ );
464
+ })}
465
+ {d.cves.length > 3 && (
466
+ <Text color="gray" dimColor> ...and {d.cves.length - 3} more</Text>
467
+ )}
468
+ {d.suggestedUpgrade && (
469
+ <Box marginLeft={4}>
470
+ <Text color="green">↑ upgrade to {d.suggestedUpgrade}</Text>
471
+ </Box>
472
+ )}
473
+ </Box>
474
+ ))}
475
+ </Box>
476
+ )}
477
+
478
+ {warnDeps.length > 0 && (
479
+ <Box flexDirection="column" marginTop={blockedDeps.length > 0 ? 1 : 0}>
480
+ <Text color="yellow" bold> WARNING ({warnDeps.length})</Text>
481
+ {warnDeps.map((d) => (
482
+ <Box key={d.name} marginLeft={2}>
483
+ <Text color="yellow">⚠ </Text>
484
+ <Text>{d.name}</Text>
485
+ <Text color="gray">@{d.version}</Text>
486
+ {d.cvesHigh > 0 && <Text color="yellow"> {d.cvesHigh} high CVE(s)</Text>}
487
+ {d.score !== null && <Text color="yellow"> risk {d.score}/100</Text>}
488
+ </Box>
489
+ ))}
490
+ </Box>
491
+ )}
492
+
493
+ {safeDeps.length > 0 && (
494
+ <Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length) > 0 ? 1 : 0}>
495
+ <Text color="green" bold> SAFE ({safeDeps.length})</Text>
496
+ {safeDeps.map((d) => (
497
+ <Box key={d.name} marginLeft={2}>
498
+ <Text color="green">✓ </Text>
499
+ <Text>{d.name}</Text>
500
+ <Text color="gray">@{d.version}</Text>
501
+ {d.onChain && d.score !== null && (
502
+ <Text color="green"> {d.score}/100</Text>
503
+ )}
504
+ </Box>
505
+ ))}
506
+ </Box>
507
+ )}
508
+ </Box>
509
+ )}
510
+
511
+ {!scanning && (
512
+ <Box flexDirection="column" marginTop={1}>
513
+ <Text color="gray">────────────────────────────────────────</Text>
514
+
515
+ {blockedDeps.length === 0 && (
516
+ <StatusLine label="Install via npm" status={installStatus} />
517
+ )}
518
+
519
+ <Box marginTop={1}>
520
+ <Text color={blockedDeps.length > 0 ? 'red' : totalCves > 0 ? 'yellow' : 'green'} bold>
521
+ {deps.length} packages scanned: {blockedDeps.length} blocked, {warnDeps.length} warnings, {totalCves} CVEs
522
+ </Text>
523
+ </Box>
524
+
525
+ {blockedDeps.length > 0 && (
526
+ <Text color="red">Fix blocked packages before installing. Upgrade to safe versions above.</Text>
527
+ )}
528
+ </Box>
529
+ )}
530
+
531
+ {error && <Text color="red">{error}</Text>}
532
+ {installStatus === 'done' && <Text color="green" bold>Done.</Text>}
533
+ </Box>
534
+ );
535
+ }
536
+
537
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
538
+
329
539
  function getBestUpgradeVersion(cves: OSVVulnerability[], currentVersion: string): string | null {
330
540
  let highest: string | null = null;
331
541
  for (const cve of cves) {
@@ -346,17 +556,3 @@ function compareSemver(a: string, b: string): number {
346
556
  }
347
557
  return 0;
348
558
  }
349
-
350
- function getTargetPackages(name?: string, ver?: string): Array<{ name: string; version: string }> {
351
- if (name) return [{ name, version: ver || 'latest' }];
352
-
353
- const pkgJsonPath = path.resolve('package.json');
354
- if (!fs.existsSync(pkgJsonPath)) return [];
355
-
356
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
357
- const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
358
- return Object.entries(deps).map(([n, v]) => ({
359
- name: n,
360
- version: String(v).replace(/^[\^~]/, ''),
361
- }));
362
- }
@@ -1,10 +1,11 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { getEnvOrThrow, truncateAddress, classifyRisk } from '@opm/core';
3
+ import { getEnvOrThrow, truncateAddress, classifyRisk, txUrl, contractUrl, addressUrl } from '@opm/core';
4
4
  import type { AgentEntry } from '@opm/core';
5
5
  import { Header } from '../components/Header';
6
6
  import { StatusLine, type Status } from '../components/StatusLine';
7
7
  import { RiskBadge } from '../components/RiskBadge';
8
+ import { Hyperlink } from '../components/Hyperlink';
8
9
  import { computeChecksum, signChecksumAsync } from '../services/signature';
9
10
  import { resolveENSName } from '../services/ens';
10
11
  import { registerPackageOnChain } from '../services/contract';
@@ -71,7 +72,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
71
72
  if (!name || !version) throw new Error('package.json missing name or version');
72
73
  setPkgLabel(`${name}@${version}`);
73
74
 
74
- const privateKey = getEnvOrThrow('OPM_PRIVATE_KEY');
75
+ const privateKey = getEnvOrThrow('OPM_SIGNING_KEY', 'OPM_PRIVATE_KEY');
75
76
 
76
77
  updateStep('pack', 'running');
77
78
  const tarball = execSync('npm pack --json 2>/dev/null', { encoding: 'utf-8' });
@@ -242,26 +243,44 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
242
243
  <Box flexDirection="column" marginTop={1}>
243
244
  <Text color="gray">────────────────────────────────────────</Text>
244
245
  <Text color="white" bold> Agent Results</Text>
245
- {result.agents.map((agent) => (
246
- <Box key={agent.agent_id} flexDirection="column" marginLeft={2} marginTop={1}>
247
- <Box>
248
- <Text color="white" bold>{agent.agent_id}</Text>
249
- <Text color="gray"> ({agent.model}) </Text>
250
- <Text color={riskColor(agent.result.risk_score)} bold>
251
- {agent.result.risk_score}/100
252
- </Text>
253
- <Text color="gray"> {agent.result.risk_level}</Text>
254
- </Box>
255
- <Box marginLeft={2}>
256
- <Text color="gray" wrap="wrap">{agent.result.reasoning.slice(0, 200)}</Text>
257
- </Box>
258
- {agent.result.vulnerabilities.length > 0 && (
246
+ {result.agents.map((agent) => {
247
+ const intel = agent.model_intelligence || 0;
248
+ const coding = agent.model_coding || 0;
249
+ const weight = agent.model_weight || 0;
250
+ return (
251
+ <Box key={agent.agent_id} flexDirection="column" marginLeft={2} marginTop={1}>
252
+ <Box>
253
+ <Text color="white" bold>{agent.agent_id}</Text>
254
+ <Text color="gray"> ({agent.model}) </Text>
255
+ <Text color={riskColor(agent.result.risk_score)} bold>
256
+ {agent.result.risk_score}/100
257
+ </Text>
258
+ <Text color="gray"> {agent.result.risk_level}</Text>
259
+ </Box>
259
260
  <Box marginLeft={2}>
260
- <Text color="yellow">{agent.result.vulnerabilities.length} vulnerabilities found</Text>
261
+ <Text color="magenta">AI Index: {intel}</Text>
262
+ <Text color="gray"> | </Text>
263
+ <Text color="blue">Coding: {coding}</Text>
264
+ <Text color="gray"> | </Text>
265
+ <Text color="cyan">Weight: {weight}</Text>
261
266
  </Box>
262
- )}
263
- </Box>
264
- ))}
267
+ <Box marginLeft={2}>
268
+ <Text color="gray" wrap="wrap">{agent.result.reasoning.slice(0, 200)}</Text>
269
+ </Box>
270
+ {agent.result.vulnerabilities.length > 0 && (
271
+ <Box marginLeft={2}>
272
+ <Text color="yellow">{agent.result.vulnerabilities.length} vulnerabilities found</Text>
273
+ </Box>
274
+ )}
275
+ {agent.score_tx_hash && (
276
+ <Box marginLeft={2}>
277
+ <Text color="gray">⛓ </Text>
278
+ <Hyperlink url={txUrl(agent.score_tx_hash)} label={`score tx ${agent.score_tx_hash.slice(0, 10)}…`} color="cyan" />
279
+ </Box>
280
+ )}
281
+ </Box>
282
+ );
283
+ })}
265
284
  </Box>
266
285
  )}
267
286
 
@@ -269,7 +288,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
269
288
  <Box flexDirection="column" marginTop={1}>
270
289
  <Text color="gray">────────────────────────────────────────</Text>
271
290
  <Box>
272
- <Text color="white" bold> Aggregate Risk: </Text>
291
+ <Text color="white" bold> Aggregate Risk (intelligence-weighted): </Text>
273
292
  <RiskBadge level={classifyRisk(result.riskScore)} score={result.riskScore} />
274
293
  </Box>
275
294
  </Box>
@@ -294,6 +313,18 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
294
313
  </Box>
295
314
  )}
296
315
  <StatusLine label="Register on-chain" status={steps.register} detail={result.txHash?.slice(0, 16)} />
316
+ {result.txHash && (
317
+ <Box flexDirection="column" marginLeft={4}>
318
+ <Box>
319
+ <Text color="gray">⛓ </Text>
320
+ <Hyperlink url={txUrl(result.txHash)} label={`tx ${result.txHash.slice(0, 10)}…`} color="green" />
321
+ </Box>
322
+ <Box>
323
+ <Text color="gray">📋 </Text>
324
+ <Hyperlink url={contractUrl()} label="OPM Registry Contract" color="cyan" />
325
+ </Box>
326
+ </Box>
327
+ )}
297
328
  </>
298
329
  )}
299
330
 
@@ -309,7 +340,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
309
340
  </Box>
310
341
  {!result.reportURI.startsWith('local://') && (
311
342
  <Box marginLeft={2}>
312
- <Text color="blue">{result.reportURI}</Text>
343
+ <Hyperlink url={result.reportURI} />
313
344
  </Box>
314
345
  )}
315
346
  </Box>
@@ -0,0 +1,227 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { txUrl, contractUrl, addressUrl } from '@opm/core';
4
+ import { Header } from '../components/Header';
5
+ import { StatusLine, type Status } from '../components/StatusLine';
6
+ import { Hyperlink } from '../components/Hyperlink';
7
+ import { registerAgentOnChain } from '../services/contract';
8
+ import { runBenchmarkSuite, type BenchmarkRunResult } from '@opm/scanner';
9
+
10
+ type StepStatus = Status;
11
+
12
+ interface Steps {
13
+ validate: StepStatus;
14
+ benchmark: StepStatus;
15
+ zkproof: StepStatus;
16
+ register: StepStatus;
17
+ }
18
+
19
+ interface RegisterResult {
20
+ agentName?: string;
21
+ model?: string;
22
+ benchmarkResult?: BenchmarkRunResult;
23
+ txHash?: string;
24
+ agentAddress?: string;
25
+ rejected?: boolean;
26
+ rejectReason?: string;
27
+ }
28
+
29
+ interface RegisterAgentCommandProps {
30
+ agentName: string;
31
+ model: string;
32
+ systemPrompt?: string;
33
+ }
34
+
35
+ export function RegisterAgentCommand({ agentName, model, systemPrompt }: RegisterAgentCommandProps) {
36
+ const [steps, setSteps] = useState<Steps>({
37
+ validate: 'pending',
38
+ benchmark: 'pending',
39
+ zkproof: 'pending',
40
+ register: 'pending',
41
+ });
42
+ const [result, setResult] = useState<RegisterResult>({});
43
+ const [error, setError] = useState<string | null>(null);
44
+ const [logs, setLogs] = useState<string[]>([]);
45
+
46
+ const updateStep = (key: keyof Steps, status: StepStatus) =>
47
+ setSteps((prev) => ({ ...prev, [key]: status }));
48
+
49
+ useEffect(() => {
50
+ runRegistration().catch((err) => setError(String(err)));
51
+ }, []);
52
+
53
+ async function runRegistration() {
54
+ setResult({ agentName, model });
55
+
56
+ updateStep('validate', 'running');
57
+
58
+ if (!agentName || agentName.length < 2) {
59
+ throw new Error('Agent name must be at least 2 characters');
60
+ }
61
+ if (!model) {
62
+ throw new Error('Model identifier is required (e.g. anthropic/claude-sonnet-4-20250514)');
63
+ }
64
+
65
+ if (!process.env.AGENT_PRIVATE_KEY) {
66
+ throw new Error('AGENT_PRIVATE_KEY required — this wallet becomes the agent identity');
67
+ }
68
+ if (!process.env.OPENROUTER_API_KEY && !process.env.OPENAI_API_KEY) {
69
+ throw new Error('OPENROUTER_API_KEY or OPENAI_API_KEY required to run benchmarks');
70
+ }
71
+
72
+ updateStep('validate', 'done');
73
+
74
+ updateStep('benchmark', 'running');
75
+ const benchResult = await runBenchmarkSuite(
76
+ { name: agentName, model, systemPrompt },
77
+ (msg) => setLogs((prev) => [...prev.slice(-12), msg]),
78
+ );
79
+ setResult((r) => ({ ...r, benchmarkResult: benchResult }));
80
+
81
+ if (!benchResult.zkProof.passed || benchResult.accuracyPct < 100) {
82
+ updateStep('benchmark', 'error');
83
+ updateStep('zkproof', 'error');
84
+ updateStep('register', 'blocked');
85
+ setResult((r) => ({
86
+ ...r,
87
+ rejected: true,
88
+ rejectReason: `Agent achieved ${benchResult.accuracyPct}% accuracy (100% required). ` +
89
+ `Failed ${benchResult.failed}/${benchResult.total} benchmark cases.`,
90
+ }));
91
+ return;
92
+ }
93
+ updateStep('benchmark', 'done');
94
+
95
+ updateStep('zkproof', 'running');
96
+ if (!benchResult.verified) {
97
+ updateStep('zkproof', 'error');
98
+ updateStep('register', 'blocked');
99
+ setResult((r) => ({
100
+ ...r,
101
+ rejected: true,
102
+ rejectReason: 'ZK proof verification failed — integrity check did not pass',
103
+ }));
104
+ return;
105
+ }
106
+ setLogs((prev) => [...prev, `ZK proof hash: ${benchResult.zkProof.accuracyProof.slice(0, 24)}…`]);
107
+ updateStep('zkproof', 'done');
108
+
109
+ updateStep('register', 'running');
110
+ try {
111
+ const proofStr = benchResult.zkProof.accuracyProof;
112
+ const promptStr = systemPrompt || 'default-opm-security-prompt';
113
+ const txHash = await registerAgentOnChain(agentName, model, promptStr, proofStr);
114
+ setResult((r) => ({ ...r, txHash }));
115
+ setLogs((prev) => [...prev, `Agent registered on-chain ✓`]);
116
+ } catch (err: any) {
117
+ const msg = err?.shortMessage || err?.message || 'failed';
118
+ setLogs((prev) => [...prev, `Registration: ${msg}`]);
119
+ if (msg.includes('already')) {
120
+ setResult((r) => ({ ...r, rejected: true, rejectReason: 'Agent wallet is already registered' }));
121
+ updateStep('register', 'error');
122
+ return;
123
+ }
124
+ }
125
+ updateStep('register', 'done');
126
+ }
127
+
128
+ const riskColor = (pct: number) => (pct >= 100 ? 'green' : pct >= 70 ? 'yellow' : 'red');
129
+
130
+ return (
131
+ <Box flexDirection="column">
132
+ <Header subtitle="register-agent" />
133
+ <Text color="white" bold> Registering agent: {agentName}</Text>
134
+ <Text color="gray"> Model: {model}</Text>
135
+ <Text> </Text>
136
+
137
+ <StatusLine label="Validate configuration" status={steps.validate} />
138
+ <StatusLine label="Run benchmark suite (10 cases)" status={steps.benchmark} />
139
+
140
+ {logs.length > 0 && (
141
+ <Box flexDirection="column" marginLeft={4}>
142
+ {logs.map((log, i) => (
143
+ <Text key={i} color="gray">{log}</Text>
144
+ ))}
145
+ </Box>
146
+ )}
147
+
148
+ {result.benchmarkResult && (
149
+ <Box flexDirection="column" marginTop={1}>
150
+ <Text color="gray">────────────────────────────────────────</Text>
151
+ <Text color="white" bold> Benchmark Results</Text>
152
+ <Box flexDirection="column" marginLeft={2} marginTop={1}>
153
+ {result.benchmarkResult.results.map((r) => (
154
+ <Box key={r.caseId}>
155
+ <Text color={r.verdict === 'PASS' ? 'green' : 'red'}>
156
+ {r.verdict === 'PASS' ? '✓' : '✗'}{' '}
157
+ </Text>
158
+ <Text color="white">{r.category}</Text>
159
+ <Text color="gray"> — expected {r.expectedLevel}, got {r.actualLevel}</Text>
160
+ <Text color="gray"> (score: {r.actualScore})</Text>
161
+ </Box>
162
+ ))}
163
+ </Box>
164
+ <Box marginLeft={2} marginTop={1}>
165
+ <Text color={riskColor(result.benchmarkResult.accuracyPct)} bold>
166
+ Accuracy: {result.benchmarkResult.passed}/{result.benchmarkResult.total}{' '}
167
+ ({result.benchmarkResult.accuracyPct}%)
168
+ </Text>
169
+ </Box>
170
+ </Box>
171
+ )}
172
+
173
+ <Text> </Text>
174
+ <StatusLine label="ZK proof verification" status={steps.zkproof} />
175
+ {result.benchmarkResult?.zkProof && steps.zkproof === 'done' && (
176
+ <Box flexDirection="column" marginLeft={4}>
177
+ <Text color="gray">Commitment: {result.benchmarkResult.zkProof.commitment.expectedHash.slice(0, 24)}…</Text>
178
+ <Text color="gray">Proof: {result.benchmarkResult.zkProof.accuracyProof.slice(0, 24)}…</Text>
179
+ <Text color="green">✓ Zero-knowledge proof verified — accuracy proven without revealing test data</Text>
180
+ </Box>
181
+ )}
182
+
183
+ <StatusLine label="Register agent on-chain" status={steps.register} />
184
+ {result.txHash && (
185
+ <Box flexDirection="column" marginLeft={4}>
186
+ <Box>
187
+ <Text color="gray">⛓ </Text>
188
+ <Hyperlink url={txUrl(result.txHash)} label={`tx ${result.txHash.slice(0, 10)}…`} color="green" />
189
+ </Box>
190
+ <Box>
191
+ <Text color="gray">📋 </Text>
192
+ <Hyperlink url={contractUrl()} label="OPM Registry Contract" color="cyan" />
193
+ </Box>
194
+ </Box>
195
+ )}
196
+
197
+ {result.rejected && (
198
+ <Box flexDirection="column" marginTop={1}>
199
+ <Text color="gray">────────────────────────────────────────</Text>
200
+ <Text color="red" bold>✗ REGISTRATION REJECTED</Text>
201
+ <Box marginLeft={2}>
202
+ <Text color="red" wrap="wrap">{result.rejectReason}</Text>
203
+ </Box>
204
+ {result.benchmarkResult && result.benchmarkResult.failureReasons.length > 0 && (
205
+ <Box flexDirection="column" marginLeft={2} marginTop={1}>
206
+ <Text color="yellow" bold>Failure details:</Text>
207
+ {result.benchmarkResult.failureReasons.map((reason, i) => (
208
+ <Text key={i} color="yellow" wrap="wrap"> • {reason}</Text>
209
+ ))}
210
+ </Box>
211
+ )}
212
+ </Box>
213
+ )}
214
+
215
+ {!result.rejected && steps.register === 'done' && (
216
+ <Box flexDirection="column" marginTop={1}>
217
+ <Text color="gray">────────────────────────────────────────</Text>
218
+ <Text color="green" bold>✓ Agent "{agentName}" registered successfully</Text>
219
+ <Text color="gray"> Your agent is now authorized to submit security scores on-chain.</Text>
220
+ <Text color="gray"> It will participate in the next package scan alongside existing agents.</Text>
221
+ </Box>
222
+ )}
223
+
224
+ {error && <Text color="red">Error: {error}</Text>}
225
+ </Box>
226
+ );
227
+ }