opmsec 0.1.4 → 0.1.5

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 (139) hide show
  1. package/.env.example +1 -0
  2. package/README.md +71 -275
  3. package/bun.lock +3 -3
  4. package/docs/architecture/agents.mdx +11 -59
  5. package/docs/architecture/benchmarks.mdx +20 -46
  6. package/docs/architecture/overview.mdx +31 -38
  7. package/docs/architecture/scanner.mdx +11 -37
  8. package/docs/cli/audit.mdx +9 -12
  9. package/docs/cli/check.mdx +12 -26
  10. package/docs/cli/fix.mdx +10 -30
  11. package/docs/cli/info.mdx +12 -19
  12. package/docs/cli/install.mdx +27 -39
  13. package/docs/cli/push.mdx +40 -57
  14. package/docs/cli/register-agent.mdx +21 -53
  15. package/docs/cli/view.mdx +12 -29
  16. package/docs/concepts/ens-records.mdx +44 -0
  17. package/docs/concepts/multi-agent-consensus.mdx +18 -36
  18. package/docs/concepts/on-chain-registry.mdx +22 -49
  19. package/docs/concepts/security-model.mdx +20 -52
  20. package/docs/concepts/zk-agent-verification.mdx +26 -64
  21. package/docs/contract/events.mdx +13 -74
  22. package/docs/contract/functions.mdx +40 -126
  23. package/docs/contract/overview.mdx +17 -36
  24. package/docs/introduction.mdx +22 -25
  25. package/docs/mint.json +1 -0
  26. package/docs/quickstart.mdx +34 -70
  27. package/docs/system-design.png +0 -0
  28. package/package.json +5 -5
  29. package/packages/cli/src/commands/author-view.tsx +87 -2
  30. package/packages/cli/src/commands/check.tsx +18 -5
  31. package/packages/cli/src/commands/fix.tsx +25 -12
  32. package/packages/cli/src/commands/info.tsx +92 -4
  33. package/packages/cli/src/commands/install.tsx +54 -8
  34. package/packages/cli/src/commands/push.tsx +112 -0
  35. package/packages/cli/src/commands/register-agent.tsx +72 -31
  36. package/packages/cli/src/index.tsx +4 -4
  37. package/packages/cli/src/services/ens-records.ts +525 -0
  38. package/packages/core/src/benchmarks.ts +116 -0
  39. package/packages/core/src/constants.ts +18 -6
  40. package/packages/core/src/model-rankings.ts +40 -15
  41. package/packages/core/src/types.ts +10 -0
  42. package/packages/core/src/utils.ts +3 -3
  43. package/packages/scanner/src/index.ts +2 -1
  44. package/packages/scanner/src/queue/memory-queue.ts +7 -2
  45. package/packages/scanner/src/services/benchmark-runner.ts +86 -1
  46. package/packages/scanner/src/services/fileverse.ts +61 -12
  47. package/packages/web/.next/BUILD_ID +1 -1
  48. package/packages/web/.next/app-build-manifest.json +7 -18
  49. package/packages/web/.next/build-manifest.json +6 -19
  50. package/packages/web/.next/images-manifest.json +1 -1
  51. package/packages/web/.next/required-server-files.json +2 -2
  52. package/packages/web/.next/server/app/_not-found/page.js +2 -2
  53. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  54. package/packages/web/.next/server/app/_not-found.html +1 -1
  55. package/packages/web/.next/server/app/_not-found.rsc +4 -2
  56. package/packages/web/.next/server/app/index.html +6 -1
  57. package/packages/web/.next/server/app/index.rsc +5 -3
  58. package/packages/web/.next/server/app/page.js +272 -2
  59. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  60. package/packages/web/.next/server/app-paths-manifest.json +0 -1
  61. package/packages/web/.next/server/middleware-build-manifest.js +22 -1
  62. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -1
  63. package/packages/web/.next/server/next-font-manifest.js +1 -1
  64. package/packages/web/.next/server/next-font-manifest.json +1 -1
  65. package/packages/web/.next/server/pages/404.html +1 -1
  66. package/packages/web/.next/server/pages/500.html +1 -1
  67. package/packages/web/.next/server/pages-manifest.json +1 -6
  68. package/packages/web/.next/server/server-reference-manifest.js +1 -1
  69. package/packages/web/.next/server/server-reference-manifest.json +5 -1
  70. package/packages/web/.next/server/webpack-runtime.js +209 -1
  71. package/packages/web/.next/static/chunks/174-5b5efcb3b8efcc01.js +1 -0
  72. package/packages/web/.next/static/chunks/app/layout-de8e841104500505.js +1 -0
  73. package/packages/web/.next/static/chunks/app/layout.js +69 -0
  74. package/packages/web/.next/static/chunks/app/page-7e086379698b9fb0.js +1 -0
  75. package/packages/web/.next/static/chunks/app/page.js +357 -0
  76. package/packages/web/.next/static/chunks/{main-ee293fa6aa18bdd1.js → main-4e8d71b5ef7ee7e3.js} +1 -1
  77. package/packages/web/.next/static/chunks/webpack-0dcd67569eb46132.js +1 -0
  78. package/packages/web/.next/static/chunks/webpack.js +1393 -0
  79. package/packages/web/.next/static/css/102562cf2d0ae9b0.css +3 -0
  80. package/packages/web/.next/static/development/_buildManifest.js +1 -0
  81. package/packages/web/.next/static/development/_ssgManifest.js +1 -0
  82. package/packages/web/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  83. package/packages/web/.next/static/media/747892c23ea88013-s.woff2 +0 -0
  84. package/packages/web/.next/static/media/8d697b304b401681-s.woff2 +0 -0
  85. package/packages/web/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  86. package/packages/web/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
  87. package/packages/web/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  88. package/packages/web/.next/static/webpack/16f18baa938a434c.webpack.hot-update.json +1 -0
  89. package/packages/web/.next/static/webpack/5fe9fe8578f9c3d2.webpack.hot-update.json +1 -0
  90. package/packages/web/.next/static/webpack/653e365406c0d9ac.webpack.hot-update.json +1 -0
  91. package/packages/web/.next/static/webpack/6800169a899e3a8b.webpack.hot-update.json +1 -0
  92. package/packages/web/.next/static/webpack/73c7d02260cc80e4.webpack.hot-update.json +1 -0
  93. package/packages/web/.next/static/webpack/a2d85d19aa028de1.webpack.hot-update.json +1 -0
  94. package/packages/web/.next/static/webpack/app/layout.16f18baa938a434c.hot-update.js +22 -0
  95. package/packages/web/.next/static/webpack/app/layout.5fe9fe8578f9c3d2.hot-update.js +22 -0
  96. package/packages/web/.next/static/webpack/app/layout.653e365406c0d9ac.hot-update.js +22 -0
  97. package/packages/web/.next/static/webpack/app/layout.6800169a899e3a8b.hot-update.js +22 -0
  98. package/packages/web/.next/static/webpack/app/layout.73c7d02260cc80e4.hot-update.js +22 -0
  99. package/packages/web/.next/static/webpack/app/layout.a2d85d19aa028de1.hot-update.js +22 -0
  100. package/packages/web/.next/static/webpack/app/page.653e365406c0d9ac.hot-update.js +22 -0
  101. package/packages/web/.next/static/webpack/app/page.6800169a899e3a8b.hot-update.js +22 -0
  102. package/packages/web/.next/static/webpack/app/page.73c7d02260cc80e4.hot-update.js +22 -0
  103. package/packages/web/.next/static/webpack/app/page.a2d85d19aa028de1.hot-update.js +22 -0
  104. package/packages/web/.next/static/webpack/webpack.16f18baa938a434c.hot-update.js +12 -0
  105. package/packages/web/.next/static/webpack/webpack.5fe9fe8578f9c3d2.hot-update.js +12 -0
  106. package/packages/web/.next/static/webpack/webpack.653e365406c0d9ac.hot-update.js +12 -0
  107. package/packages/web/.next/static/webpack/webpack.6800169a899e3a8b.hot-update.js +12 -0
  108. package/packages/web/.next/static/webpack/webpack.73c7d02260cc80e4.hot-update.js +12 -0
  109. package/packages/web/.next/static/webpack/webpack.a2d85d19aa028de1.hot-update.js +12 -0
  110. package/packages/web/.next/trace +2 -2
  111. package/packages/web/app/globals.css +197 -51
  112. package/packages/web/app/layout.tsx +6 -3
  113. package/packages/web/app/page.tsx +791 -312
  114. package/packages/web/bun.lock +66 -105
  115. package/packages/web/next.config.ts +8 -1
  116. package/packages/web/package.json +5 -2
  117. package/packages/web/postcss.config.mjs +2 -2
  118. package/packages/web/public/apple-icon.png +1 -0
  119. package/packages/web/public/dependency-bottleneck.png +0 -0
  120. package/packages/web/public/icon-dark-32x32.png +1 -0
  121. package/packages/web/public/icon-light-32x32.png +1 -0
  122. package/packages/web/public/icon.svg +1 -0
  123. package/packages/web/public/nextjs-cve-announcement.png +0 -0
  124. package/packages/web/public/phantomraven-npm-attack.png +0 -0
  125. package/packages/web/public/placeholder-logo.png +1 -0
  126. package/packages/web/public/placeholder-logo.svg +1 -0
  127. package/packages/web/public/placeholder-user.jpg +1 -0
  128. package/packages/web/public/placeholder.jpg +1 -0
  129. package/packages/web/public/placeholder.svg +1 -0
  130. package/packages/web/public/react-cve-meme.png +0 -0
  131. package/packages/web/public/wallet-drain-exploit.png +0 -0
  132. package/packages/web/styles/globals.css +125 -0
  133. package/packages/web/.next/static/chunks/app/layout-28a489fb4398663f.js +0 -1
  134. package/packages/web/.next/static/chunks/app/page-e58ccdb78625bce6.js +0 -1
  135. package/packages/web/.next/static/chunks/webpack-e1ae44446e7f7355.js +0 -1
  136. package/packages/web/.next/static/css/21d69157e271f2ab.css +0 -3
  137. package/packages/web/tailwind.config.ts +0 -48
  138. /package/packages/web/.next/static/{2XIFCTTKVZwN_RsNE-Rrr → 0esGzFBCzREfVwijEGDfL}/_buildManifest.js +0 -0
  139. /package/packages/web/.next/static/{2XIFCTTKVZwN_RsNE-Rrr → 0esGzFBCzREfVwijEGDfL}/_ssgManifest.js +0 -0
@@ -12,6 +12,8 @@ import { checkPackageWithChainPatrol } from '../services/chainpatrol';
12
12
  import { queryOSV, getOSVSeverity, getFixedVersion, type OSVVulnerability } from '../services/osv';
13
13
  import { resolveVersion, findSafeVersion, isENSVersion, type ResolvedVersion } from '../services/version';
14
14
  import { resolveAddress } from '../services/ens';
15
+ import { readOPMRecords, readPackageENSRecords } from '../services/ens-records';
16
+ import type { OPMENSRecords } from '@opm/core';
15
17
  import { execSync } from 'child_process';
16
18
  import * as fs from 'fs';
17
19
  import * as path from 'path';
@@ -92,6 +94,7 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
92
94
  });
93
95
  const [result, setResult] = useState<SecurityResult | null>(null);
94
96
  const [ensDetail, setEnsDetail] = useState<string | undefined>(undefined);
97
+ const [ensRecords, setEnsRecords] = useState<OPMENSRecords>({});
95
98
  const [error, setError] = useState<string | null>(null);
96
99
  const [done, setDone] = useState(false);
97
100
 
@@ -120,6 +123,22 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
120
123
  r.ensName = resolved.ensName;
121
124
  setResult({ ...r });
122
125
  setEnsDetail(`${resolved.ensName} → v${resolved.version} (${resolved.reason})`);
126
+
127
+ if (resolved.ensName) {
128
+ const [opmRecs, pkgRecs] = await Promise.allSettled([
129
+ readOPMRecords(resolved.ensName),
130
+ readPackageENSRecords(resolved.ensName, packageName),
131
+ ]);
132
+ const merged: OPMENSRecords = {};
133
+ if (pkgRecs.status === 'fulfilled') Object.assign(merged, pkgRecs.value);
134
+ if (opmRecs.status === 'fulfilled') {
135
+ for (const [k, v] of Object.entries(opmRecs.value)) {
136
+ if (v && !(merged as any)[k]) (merged as any)[k] = v;
137
+ }
138
+ }
139
+ setEnsRecords(merged);
140
+ }
141
+
123
142
  update('ens', 'done');
124
143
  } catch (err: any) {
125
144
  setEnsDetail(err?.message || 'ENS resolution failed');
@@ -279,13 +298,25 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
279
298
  {result.resolved.authorAddress && (
280
299
  <Text color="gray"> ({truncateAddress(result.resolved.authorAddress)})</Text>
281
300
  )}
282
- <Text color="green"> on-chain</Text>
301
+ <Text color="green"> on-chain</Text>
283
302
  </Box>
284
303
  <Box>
285
304
  <Text color="gray">Version: </Text>
286
305
  <Text color="cyan">{result.resolvedVersion}</Text>
287
306
  <Text color="gray"> (safest on-chain version)</Text>
288
307
  </Box>
308
+ {ensRecords.fileverse && (
309
+ <Box>
310
+ <Text color="gray">Report: </Text>
311
+ <Text color="green">{ensRecords.fileverse.length > 50 ? ensRecords.fileverse.slice(0, 50) + '...' : ensRecords.fileverse}</Text>
312
+ </Box>
313
+ )}
314
+ {ensRecords.riskScore && (
315
+ <Box>
316
+ <Text color="gray">Risk: </Text>
317
+ <Text color="cyan">{ensRecords.riskScore}/100 (from ENS)</Text>
318
+ </Box>
319
+ )}
289
320
  </Box>
290
321
  )}
291
322
 
@@ -423,6 +454,12 @@ function SingleInstall({ packageName, version }: { packageName: string; version?
423
454
  <Text color="green">{result.ensName}</Text>
424
455
  </Box>
425
456
  )}
457
+ {ensRecords.fileverse && (
458
+ <Box marginLeft={2}>
459
+ <Text color="gray">Fileverse:</Text>
460
+ <Text color="green"> {ensRecords.fileverse.length > 45 ? ensRecords.fileverse.slice(0, 45) + '...' : ensRecords.fileverse}</Text>
461
+ </Box>
462
+ )}
426
463
  {result.warning && !result.blocked && !result.autoBumped && (
427
464
  <Box marginLeft={2}>
428
465
  <Text color="yellow">⚠ Vulnerabilities detected — review before using in production</Text>
@@ -634,16 +671,25 @@ function BulkInstall() {
634
671
  return;
635
672
  }
636
673
 
637
- // Build npm install command with correct versions
674
+ // Update package.json with resolved versions before installing
638
675
  const bumpedDeps = checked.filter((d) => d.autoBumped || d.ensResolved);
676
+ if (bumpedDeps.length > 0) {
677
+ const freshPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
678
+ for (const dep of bumpedDeps) {
679
+ const resolved = `^${dep.version}`;
680
+ if (freshPkg.dependencies && dep.name in freshPkg.dependencies) {
681
+ freshPkg.dependencies[dep.name] = resolved;
682
+ }
683
+ if (freshPkg.devDependencies && dep.name in freshPkg.devDependencies) {
684
+ freshPkg.devDependencies[dep.name] = resolved;
685
+ }
686
+ }
687
+ fs.writeFileSync(pkgPath, JSON.stringify(freshPkg, null, 2) + '\n');
688
+ }
689
+
639
690
  setInstallStatus('running');
640
691
  try {
641
- if (bumpedDeps.length > 0) {
642
- const args = checked.map((d) => `${d.name}@${d.version}`).join(' ');
643
- execSync(`npm install ${args}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
644
- } else {
645
- execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
646
- }
692
+ execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
647
693
  } catch { /* non-fatal */ }
648
694
  setInstallStatus('done');
649
695
  }
@@ -9,6 +9,7 @@ import { Hyperlink } from '../components/Hyperlink';
9
9
  import { computeChecksum, signChecksumAsync } from '../services/signature';
10
10
  import { resolveENSName } from '../services/ens';
11
11
  import { registerPackageOnChain } from '../services/contract';
12
+ import { writeENSRecords, buildOPMRecords, readOPMRecords, createPackageSubname, setENSContenthash, parseFileverseLink, readFileverseContentHash } from '../services/ens-records';
12
13
  import { enqueueScan } from '@opm/scanner';
13
14
  import * as fs from 'fs';
14
15
  import * as path from 'path';
@@ -23,6 +24,7 @@ interface Steps {
23
24
  scan: StepStatus;
24
25
  publish: StepStatus;
25
26
  register: StepStatus;
27
+ ensRecords: StepStatus;
26
28
  }
27
29
 
28
30
  interface PushResult {
@@ -39,6 +41,11 @@ interface PushResult {
39
41
  agents?: AgentEntry[];
40
42
  blocked?: boolean;
41
43
  blockReason?: string;
44
+ ensRecordsTx?: string;
45
+ ensRecordsChain?: string;
46
+ ensRecordsCount?: number;
47
+ ensSubname?: string;
48
+ ipfsContenthash?: string;
42
49
  }
43
50
 
44
51
  interface PushCommandProps {
@@ -50,10 +57,12 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
50
57
  const [steps, setSteps] = useState<Steps>({
51
58
  pack: 'pending', sign: 'pending', ens: 'pending',
52
59
  scan: 'pending', publish: 'pending', register: 'pending',
60
+ ensRecords: 'pending',
53
61
  });
54
62
  const [result, setResult] = useState<PushResult>({});
55
63
  const [error, setError] = useState<string | null>(null);
56
64
  const [scanLogs, setScanLogs] = useState<string[]>([]);
65
+ const [ensRecordLogs, setEnsRecordLogs] = useState<string[]>([]);
57
66
  const [pkgLabel, setPkgLabel] = useState('');
58
67
 
59
68
  const updateStep = (key: keyof Steps, status: StepStatus) =>
@@ -93,6 +102,9 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
93
102
 
94
103
  updateStep('scan', 'running');
95
104
  let scanPassed = false;
105
+ let finalReportURI: string | undefined;
106
+ let finalRiskScore: number | undefined;
107
+ let finalIpfsHash: string | undefined;
96
108
  try {
97
109
  const scanResult = await enqueueScan(name, version, (msg) =>
98
110
  setScanLogs((prev) => [...prev.slice(-8), msg]),
@@ -101,6 +113,9 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
101
113
 
102
114
  const riskScore = scanResult.report.aggregate_risk_score;
103
115
  const riskLevel = classifyRisk(riskScore);
116
+ finalReportURI = scanResult.reportURI;
117
+ finalRiskScore = riskScore;
118
+ finalIpfsHash = scanResult.ipfsHash;
104
119
 
105
120
  setResult((r) => ({
106
121
  ...r,
@@ -206,6 +221,74 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
206
221
  }
207
222
  updateStep('register', 'done');
208
223
 
224
+ // ── Write package metadata to ENS text records ──
225
+ if (ensName) {
226
+ updateStep('ensRecords', 'running');
227
+ const ensLog = (msg: string) => setEnsRecordLogs((prev) => [...prev, msg]);
228
+ try {
229
+ ensLog(`Reading existing records from ${ensName}...`);
230
+ const existingRecords = await readOPMRecords(ensName);
231
+ const records = buildOPMRecords({
232
+ packageName: name,
233
+ version,
234
+ checksum,
235
+ signature,
236
+ reportURI: finalReportURI,
237
+ riskScore: finalRiskScore,
238
+ existingPackages: existingRecords.packages,
239
+ });
240
+
241
+ const writeResult = await writeENSRecords(
242
+ ensName,
243
+ privateKey,
244
+ records,
245
+ ensLog,
246
+ );
247
+
248
+ if (writeResult) {
249
+ setResult((r) => ({
250
+ ...r,
251
+ ensRecordsTx: writeResult.txHash,
252
+ ensRecordsChain: writeResult.chain,
253
+ ensRecordsCount: writeResult.recordCount,
254
+ }));
255
+ } else {
256
+ ensLog(`Hint: signer needs ETH on Ethereum (Sepolia or Mainnet) for gas, and must be the manager of ${ensName}`);
257
+ }
258
+
259
+ let ipfsCid = finalIpfsHash;
260
+ if (!ipfsCid && finalReportURI) {
261
+ const fvLink = parseFileverseLink(finalReportURI);
262
+ if (fvLink) {
263
+ ensLog(`Reading IPFS hash from Fileverse contract ${fvLink.portalAddress.slice(0, 10)}... file #${fvLink.fileId}`);
264
+ ipfsCid = (await readFileverseContentHash(fvLink.portalAddress, fvLink.fileId, ensLog)) ?? undefined;
265
+ }
266
+ }
267
+ if (ipfsCid) {
268
+ const chResult = await setENSContenthash(ensName, privateKey, ipfsCid, ensLog);
269
+ if (chResult) {
270
+ setResult((r) => ({ ...r, ipfsContenthash: ipfsCid }));
271
+ }
272
+ }
273
+
274
+ const subResult = await createPackageSubname(
275
+ ensName,
276
+ name,
277
+ privateKey,
278
+ records,
279
+ ensLog,
280
+ );
281
+ if (subResult) {
282
+ setResult((r) => ({ ...r, ensSubname: subResult.subname }));
283
+ }
284
+ } catch (err: any) {
285
+ ensLog(`Error: ${err?.message || 'unknown'}`);
286
+ }
287
+ updateStep('ensRecords', 'done');
288
+ } else {
289
+ updateStep('ensRecords', 'skip');
290
+ }
291
+
209
292
  if (fs.existsSync(tarballFile)) fs.unlinkSync(tarballFile);
210
293
  }
211
294
 
@@ -325,6 +408,35 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
325
408
  </Box>
326
409
  </Box>
327
410
  )}
411
+ <StatusLine label="Write ENS records" status={steps.ensRecords}
412
+ detail={steps.ensRecords === 'skip' ? 'no ENS name' : steps.ensRecords === 'done' && result.ensRecordsCount ? `${result.ensRecordsCount} records` : undefined} />
413
+ {steps.ensRecords === 'done' && result.ensRecordsTx && (
414
+ <Box flexDirection="column" marginLeft={4}>
415
+ <Box>
416
+ <Text color="gray">Chain: </Text>
417
+ <Text color="cyan">{result.ensRecordsChain}</Text>
418
+ <Text color="gray"> | </Text>
419
+ <Hyperlink url={`https://${result.ensRecordsChain === 'sepolia' ? 'sepolia.' : ''}etherscan.io/tx/${result.ensRecordsTx}`} label={`tx ${result.ensRecordsTx.slice(0, 10)}...`} color="green" />
420
+ </Box>
421
+ <Box>
422
+ <Text color="gray">Records: </Text>
423
+ <Text color="white">url, opm.version, opm.checksum, opm.fileverse, opm.risk_score{result.ipfsContenthash ? ', contenthash' : ''}</Text>
424
+ </Box>
425
+ {result.ensSubname && (
426
+ <Box>
427
+ <Text color="gray">Subname: </Text>
428
+ <Text color="cyan" bold>{result.ensSubname}</Text>
429
+ </Box>
430
+ )}
431
+ </Box>
432
+ )}
433
+ {ensRecordLogs.length > 0 && (
434
+ <Box flexDirection="column" marginLeft={4}>
435
+ {ensRecordLogs.map((log, i) => (
436
+ <Text key={i} color={log.startsWith('Hint:') || log.startsWith('Error:') ? 'yellow' : 'gray'}>{log}</Text>
437
+ ))}
438
+ </Box>
439
+ )}
328
440
  </>
329
441
  )}
330
442
 
@@ -1,11 +1,12 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text } from 'ink';
3
+ import { ethers } from 'ethers';
3
4
  import { txUrl, contractUrl, addressUrl } from '@opm/core';
4
5
  import { Header } from '../components/Header';
5
6
  import { StatusLine, type Status } from '../components/StatusLine';
6
7
  import { Hyperlink } from '../components/Hyperlink';
7
8
  import { registerAgentOnChain } from '../services/contract';
8
- import { runBenchmarkSuite, type BenchmarkRunResult } from '@opm/scanner';
9
+ import { runBatchBenchmarkSuite, type BatchBenchmarkRunResult } from '@opm/scanner';
9
10
 
10
11
  type StepStatus = Status;
11
12
 
@@ -19,9 +20,12 @@ interface Steps {
19
20
  interface RegisterResult {
20
21
  agentName?: string;
21
22
  model?: string;
22
- benchmarkResult?: BenchmarkRunResult;
23
- txHash?: string;
24
23
  agentAddress?: string;
24
+ benchmarkResult?: BatchBenchmarkRunResult;
25
+ txHash?: string;
26
+ onChainProofHash?: string;
27
+ onChainPromptHash?: string;
28
+ alreadyRegistered?: boolean;
25
29
  rejected?: boolean;
26
30
  rejectReason?: string;
27
31
  }
@@ -69,10 +73,16 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
69
73
  throw new Error('OPENROUTER_API_KEY or OPENAI_API_KEY required to run benchmarks');
70
74
  }
71
75
 
76
+ // Derive agent wallet address from private key
77
+ const agentWallet = new ethers.Wallet(process.env.AGENT_PRIVATE_KEY);
78
+ setResult((r) => ({ ...r, agentAddress: agentWallet.address }));
79
+
72
80
  updateStep('validate', 'done');
73
81
 
82
+ // Single-call batch benchmark: sends all 10 cases at once,
83
+ // gets back 10 flagged/safe answers, compares to ground truth.
74
84
  updateStep('benchmark', 'running');
75
- const benchResult = await runBenchmarkSuite(
85
+ const benchResult = await runBatchBenchmarkSuite(
76
86
  { name: agentName, model, systemPrompt },
77
87
  (msg) => setLogs((prev) => [...prev.slice(-12), msg]),
78
88
  );
@@ -82,11 +92,15 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
82
92
  updateStep('benchmark', 'error');
83
93
  updateStep('zkproof', 'error');
84
94
  updateStep('register', 'blocked');
95
+ const failedCases = benchResult.results
96
+ .filter((r) => !r.passed)
97
+ .map((r) => `${r.caseId} (${r.category}): answered ${r.actualFlagged ? 'FLAGGED' : 'SAFE'}, expected ${r.expectedFlagged ? 'FLAGGED' : 'SAFE'}`);
85
98
  setResult((r) => ({
86
99
  ...r,
87
100
  rejected: true,
88
101
  rejectReason: `Agent achieved ${benchResult.accuracyPct}% accuracy (100% required). ` +
89
- `Failed ${benchResult.failed}/${benchResult.total} benchmark cases.`,
102
+ `Failed ${benchResult.failed}/${benchResult.total} cases.\n` +
103
+ failedCases.join('\n'),
90
104
  }));
91
105
  return;
92
106
  }
@@ -103,13 +117,19 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
103
117
  }));
104
118
  return;
105
119
  }
120
+
121
+ // Compute the on-chain hashes (same as registerAgentOnChain does)
122
+ const proofStr = benchResult.zkProof.accuracyProof;
123
+ const promptStr = systemPrompt || 'default-opm-security-prompt';
124
+ const onChainProofHash = ethers.keccak256(ethers.toUtf8Bytes(proofStr));
125
+ const onChainPromptHash = ethers.keccak256(ethers.toUtf8Bytes(promptStr));
126
+
127
+ setResult((r) => ({ ...r, onChainProofHash, onChainPromptHash }));
106
128
  setLogs((prev) => [...prev, `ZK proof hash: ${benchResult.zkProof.accuracyProof.slice(0, 24)}…`]);
107
129
  updateStep('zkproof', 'done');
108
130
 
109
131
  updateStep('register', 'running');
110
132
  try {
111
- const proofStr = benchResult.zkProof.accuracyProof;
112
- const promptStr = systemPrompt || 'default-opm-security-prompt';
113
133
  const txHash = await registerAgentOnChain(agentName, model, promptStr, proofStr);
114
134
  setResult((r) => ({ ...r, txHash }));
115
135
  setLogs((prev) => [...prev, `Agent registered on-chain ✓`]);
@@ -117,25 +137,29 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
117
137
  const msg = err?.shortMessage || err?.message || 'failed';
118
138
  setLogs((prev) => [...prev, `Registration: ${msg}`]);
119
139
  if (msg.includes('already')) {
120
- setResult((r) => ({ ...r, rejected: true, rejectReason: 'Agent wallet is already registered' }));
121
- updateStep('register', 'error');
140
+ setResult((r) => ({ ...r, alreadyRegistered: true }));
141
+ updateStep('register', 'done');
122
142
  return;
123
143
  }
124
144
  }
125
145
  updateStep('register', 'done');
126
146
  }
127
147
 
128
- const riskColor = (pct: number) => (pct >= 100 ? 'green' : pct >= 70 ? 'yellow' : 'red');
129
-
130
148
  return (
131
149
  <Box flexDirection="column">
132
150
  <Header subtitle="register-agent" />
133
151
  <Text color="white" bold> Registering agent: {agentName}</Text>
134
152
  <Text color="gray"> Model: {model}</Text>
153
+ {result.agentAddress && (
154
+ <Box>
155
+ <Text color="gray"> Wallet: </Text>
156
+ <Hyperlink url={addressUrl(result.agentAddress)} label={result.agentAddress} color="cyan" />
157
+ </Box>
158
+ )}
135
159
  <Text> </Text>
136
160
 
137
161
  <StatusLine label="Validate configuration" status={steps.validate} />
138
- <StatusLine label="Run benchmark suite (10 cases)" status={steps.benchmark} />
162
+ <StatusLine label="Batch benchmark (10 cases, single call)" status={steps.benchmark} />
139
163
 
140
164
  {logs.length > 0 && (
141
165
  <Box flexDirection="column" marginLeft={4}>
@@ -148,21 +172,23 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
148
172
  {result.benchmarkResult && (
149
173
  <Box flexDirection="column" marginTop={1}>
150
174
  <Text color="gray">────────────────────────────────────────</Text>
151
- <Text color="white" bold> Benchmark Results</Text>
175
+ <Text color="white" bold> Benchmark Results (flagged/safe)</Text>
152
176
  <Box flexDirection="column" marginLeft={2} marginTop={1}>
153
- {result.benchmarkResult.results.map((r) => (
177
+ {result.benchmarkResult.results.map((r, i) => (
154
178
  <Box key={r.caseId}>
155
- <Text color={r.verdict === 'PASS' ? 'green' : 'red'}>
156
- {r.verdict === 'PASS' ? '✓' : '✗'}{' '}
179
+ <Text color={r.passed ? 'green' : 'red'}>
180
+ {r.passed ? '✓' : '✗'}{' '}
157
181
  </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>
182
+ <Text color="white">Case {i + 1} ({r.category})</Text>
183
+ <Text color="gray"> — {r.actualFlagged ? 'FLAGGED' : 'SAFE'}</Text>
184
+ {!r.passed && (
185
+ <Text color="red"> (expected {r.expectedFlagged ? 'FLAGGED' : 'SAFE'})</Text>
186
+ )}
161
187
  </Box>
162
188
  ))}
163
189
  </Box>
164
190
  <Box marginLeft={2} marginTop={1}>
165
- <Text color={riskColor(result.benchmarkResult.accuracyPct)} bold>
191
+ <Text color={result.benchmarkResult.accuracyPct >= 100 ? 'green' : 'red'} bold>
166
192
  Accuracy: {result.benchmarkResult.passed}/{result.benchmarkResult.total}{' '}
167
193
  ({result.benchmarkResult.accuracyPct}%)
168
194
  </Text>
@@ -174,8 +200,14 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
174
200
  <StatusLine label="ZK proof verification" status={steps.zkproof} />
175
201
  {result.benchmarkResult?.zkProof && steps.zkproof === 'done' && (
176
202
  <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>
203
+ <Text color="gray">Commitment: {result.benchmarkResult.zkProof.commitment.expectedHash.slice(0, 24)}…</Text>
204
+ <Text color="gray">Proof: {result.benchmarkResult.zkProof.accuracyProof.slice(0, 24)}…</Text>
205
+ {result.onChainProofHash && (
206
+ <Text color="gray">On-chain proof: {result.onChainProofHash.slice(0, 24)}…</Text>
207
+ )}
208
+ {result.onChainPromptHash && (
209
+ <Text color="gray">Prompt hash: {result.onChainPromptHash.slice(0, 24)}…</Text>
210
+ )}
179
211
  <Text color="green">✓ Zero-knowledge proof verified — accuracy proven without revealing test data</Text>
180
212
  </Box>
181
213
  )}
@@ -187,6 +219,9 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
187
219
  <Text color="gray">⛓ </Text>
188
220
  <Hyperlink url={txUrl(result.txHash)} label={`tx ${result.txHash.slice(0, 10)}…`} color="green" />
189
221
  </Box>
222
+ {result.onChainProofHash && (
223
+ <Text color="gray"> ZK proof stored: {result.onChainProofHash.slice(0, 18)}…</Text>
224
+ )}
190
225
  <Box>
191
226
  <Text color="gray">📋 </Text>
192
227
  <Hyperlink url={contractUrl()} label="OPM Registry Contract" color="cyan" />
@@ -194,6 +229,20 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
194
229
  </Box>
195
230
  )}
196
231
 
232
+ {result.alreadyRegistered && !result.rejected && (
233
+ <Box flexDirection="column" marginLeft={4}>
234
+ <Text color="yellow">⚠ Agent wallet is already registered on-chain</Text>
235
+ {result.agentAddress && (
236
+ <Box>
237
+ <Text color="gray"> Agent: </Text>
238
+ <Hyperlink url={addressUrl(result.agentAddress)} label={result.agentAddress.slice(0, 10) + '…'} color="cyan" />
239
+ </Box>
240
+ )}
241
+ <Text color="gray"> Benchmark passed ✓ — ZK proof valid ✓</Text>
242
+ <Text color="gray"> Use a different AGENT_PRIVATE_KEY to register a new agent.</Text>
243
+ </Box>
244
+ )}
245
+
197
246
  {result.rejected && (
198
247
  <Box flexDirection="column" marginTop={1}>
199
248
  <Text color="gray">────────────────────────────────────────</Text>
@@ -201,18 +250,10 @@ export function RegisterAgentCommand({ agentName, model, systemPrompt }: Registe
201
250
  <Box marginLeft={2}>
202
251
  <Text color="red" wrap="wrap">{result.rejectReason}</Text>
203
252
  </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
253
  </Box>
213
254
  )}
214
255
 
215
- {!result.rejected && steps.register === 'done' && (
256
+ {!result.rejected && !result.alreadyRegistered && steps.register === 'done' && (
216
257
  <Box flexDirection="column" marginTop={1}>
217
258
  <Text color="gray">────────────────────────────────────────</Text>
218
259
  <Text color="green" bold>✓ Agent "{agentName}" registered successfully</Text>
@@ -105,14 +105,14 @@ function Help() {
105
105
  <Header />
106
106
  <Box flexDirection="column" marginLeft={2}>
107
107
  <Text color="cyan" bold>Security commands:</Text>
108
- <Text> opm push [--token t] [--otp c] Sign, scan, publish, register</Text>
108
+ <Text> opm push [--token t] [--otp c] Sign, scan, publish, register + ENS records</Text>
109
109
  <Text> opm install [pkg[@ver]] Install with on-chain security verification</Text>
110
110
  <Text> opm install pkg@ens.eth Install safest version by ENS author</Text>
111
111
  <Text> opm check Scan all deps: typosquats, CVEs, AI analysis</Text>
112
112
  <Text> opm fix Auto-fix typosquats and vulnerable versions</Text>
113
113
  <Text> opm audit Scan all deps against on-chain security data</Text>
114
- <Text> opm info {'<pkg>'} Show on-chain security info for a package</Text>
115
- <Text> opm view {'<name.eth>'} Show author profile, packages, and risk scores</Text>
114
+ <Text> opm info {'<pkg>'} Show on-chain + ENS record data for a package</Text>
115
+ <Text> opm view {'<name.eth>'} Author profile, OPM records, packages</Text>
116
116
  <Text> opm whois {'<name>'} Look up an ENS identity on OPM</Text>
117
117
  <Text> </Text>
118
118
  <Text color="cyan" bold>Agent commands:</Text>
@@ -135,7 +135,7 @@ function Help() {
135
135
  <Text> opm pack Create a tarball</Text>
136
136
  <Text> </Text>
137
137
  <Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
138
- <Text color="gray"> view name.eth → author profile, view pkg info</Text>
138
+ <Text color="gray"> view name.eth → author profile + OPM ENS records</Text>
139
139
  <Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
140
140
  <Text> </Text>
141
141
  <Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>