opmsec 0.1.3 → 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 (166) hide show
  1. package/.env.example +1 -0
  2. package/.husky/pre-commit +1 -0
  3. package/README.md +71 -275
  4. package/bun.lock +5 -5
  5. package/docs/architecture/agents.mdx +11 -59
  6. package/docs/architecture/benchmarks.mdx +20 -46
  7. package/docs/architecture/overview.mdx +31 -38
  8. package/docs/architecture/scanner.mdx +11 -37
  9. package/docs/cli/audit.mdx +9 -12
  10. package/docs/cli/check.mdx +12 -26
  11. package/docs/cli/fix.mdx +10 -30
  12. package/docs/cli/info.mdx +12 -19
  13. package/docs/cli/install.mdx +27 -39
  14. package/docs/cli/push.mdx +40 -57
  15. package/docs/cli/register-agent.mdx +21 -53
  16. package/docs/cli/view.mdx +12 -29
  17. package/docs/concepts/ens-records.mdx +44 -0
  18. package/docs/concepts/multi-agent-consensus.mdx +18 -36
  19. package/docs/concepts/on-chain-registry.mdx +22 -49
  20. package/docs/concepts/security-model.mdx +20 -52
  21. package/docs/concepts/zk-agent-verification.mdx +26 -64
  22. package/docs/contract/events.mdx +13 -74
  23. package/docs/contract/functions.mdx +40 -126
  24. package/docs/contract/overview.mdx +17 -36
  25. package/docs/introduction.mdx +22 -25
  26. package/docs/mint.json +3 -2
  27. package/docs/quickstart.mdx +34 -70
  28. package/docs/system-design.png +0 -0
  29. package/package.json +7 -6
  30. package/packages/cli/src/commands/author-view.tsx +87 -2
  31. package/packages/cli/src/commands/check.tsx +18 -5
  32. package/packages/cli/src/commands/fix.tsx +25 -12
  33. package/packages/cli/src/commands/info.tsx +92 -4
  34. package/packages/cli/src/commands/install.tsx +327 -23
  35. package/packages/cli/src/commands/push.tsx +112 -0
  36. package/packages/cli/src/commands/register-agent.tsx +72 -31
  37. package/packages/cli/src/index.tsx +7 -5
  38. package/packages/cli/src/services/ens-records.ts +525 -0
  39. package/packages/cli/src/services/version.ts +156 -5
  40. package/packages/core/src/benchmarks.ts +116 -0
  41. package/packages/core/src/constants.ts +18 -6
  42. package/packages/core/src/model-rankings.ts +40 -15
  43. package/packages/core/src/types.ts +10 -0
  44. package/packages/core/src/utils.ts +136 -1
  45. package/packages/scanner/src/index.ts +2 -1
  46. package/packages/scanner/src/queue/memory-queue.ts +7 -2
  47. package/packages/scanner/src/services/benchmark-runner.ts +86 -1
  48. package/packages/scanner/src/services/fileverse.ts +61 -12
  49. package/packages/scanner/src/services/openrouter.ts +18 -7
  50. package/packages/web/.next/BUILD_ID +1 -0
  51. package/packages/web/.next/app-path-routes-manifest.json +4 -0
  52. package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
  53. package/packages/web/.next/diagnostics/framework.json +1 -0
  54. package/packages/web/.next/export-marker.json +6 -0
  55. package/packages/web/.next/images-manifest.json +58 -0
  56. package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
  57. package/packages/web/.next/next-server.js.nft.json +1 -0
  58. package/packages/web/.next/prerender-manifest.json +54 -4
  59. package/packages/web/.next/required-server-files.json +320 -0
  60. package/packages/web/.next/routes-manifest.json +53 -1
  61. package/packages/web/.next/server/app/_not-found/page.js +2 -0
  62. package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  63. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  64. package/packages/web/.next/server/app/_not-found.html +1 -0
  65. package/packages/web/.next/server/app/_not-found.meta +8 -0
  66. package/packages/web/.next/server/app/_not-found.rsc +18 -0
  67. package/packages/web/.next/server/app/index.html +6 -0
  68. package/packages/web/.next/server/app/index.meta +7 -0
  69. package/packages/web/.next/server/app/index.rsc +22 -0
  70. package/packages/web/.next/server/app/page.js +24 -24
  71. package/packages/web/.next/server/app/page.js.nft.json +1 -0
  72. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -1
  73. package/packages/web/.next/server/chunks/611.js +6 -0
  74. package/packages/web/.next/server/chunks/778.js +30 -0
  75. package/packages/web/.next/server/functions-config-manifest.json +4 -0
  76. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -1
  77. package/packages/web/.next/server/next-font-manifest.js +1 -1
  78. package/packages/web/.next/server/next-font-manifest.json +1 -1
  79. package/packages/web/.next/server/pages/404.html +1 -0
  80. package/packages/web/.next/server/pages/500.html +1 -0
  81. package/packages/web/.next/server/pages/_app.js +1 -0
  82. package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
  83. package/packages/web/.next/server/pages/_document.js +1 -0
  84. package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
  85. package/packages/web/.next/server/pages/_error.js +19 -0
  86. package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
  87. package/packages/web/.next/server/webpack-runtime.js +2 -2
  88. package/packages/web/.next/static/0esGzFBCzREfVwijEGDfL/_buildManifest.js +1 -0
  89. package/packages/web/.next/static/0esGzFBCzREfVwijEGDfL/_ssgManifest.js +1 -0
  90. package/packages/web/.next/static/chunks/174-5b5efcb3b8efcc01.js +1 -0
  91. package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
  92. package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
  93. package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
  94. package/packages/web/.next/static/chunks/app/layout-de8e841104500505.js +1 -0
  95. package/packages/web/.next/static/chunks/app/layout.js +37 -7
  96. package/packages/web/.next/static/chunks/app/page-7e086379698b9fb0.js +1 -0
  97. package/packages/web/.next/static/chunks/app/page.js +297 -1
  98. package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
  99. package/packages/web/.next/static/chunks/main-4e8d71b5ef7ee7e3.js +1 -0
  100. package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
  101. package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  102. package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  103. package/packages/web/.next/static/chunks/webpack-0dcd67569eb46132.js +1 -0
  104. package/packages/web/.next/static/chunks/webpack.js +2 -2
  105. package/packages/web/.next/static/css/102562cf2d0ae9b0.css +3 -0
  106. package/packages/web/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  107. package/packages/web/.next/static/media/747892c23ea88013-s.woff2 +0 -0
  108. package/packages/web/.next/static/media/8d697b304b401681-s.woff2 +0 -0
  109. package/packages/web/.next/static/media/93f479601ee12b01-s.p.woff2 +0 -0
  110. package/packages/web/.next/static/media/9610d9e46709d722-s.woff2 +0 -0
  111. package/packages/web/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  112. package/packages/web/.next/static/webpack/16f18baa938a434c.webpack.hot-update.json +1 -0
  113. package/packages/web/.next/static/webpack/5fe9fe8578f9c3d2.webpack.hot-update.json +1 -0
  114. package/packages/web/.next/static/webpack/73c7d02260cc80e4.webpack.hot-update.json +1 -0
  115. package/packages/web/.next/static/webpack/a2d85d19aa028de1.webpack.hot-update.json +1 -0
  116. package/packages/web/.next/static/webpack/app/{layout.73e341375c8d429e.hot-update.js → layout.16f18baa938a434c.hot-update.js} +1 -1
  117. package/packages/web/.next/static/webpack/app/{layout.6fee6306e0f98869.hot-update.js → layout.5fe9fe8578f9c3d2.hot-update.js} +1 -1
  118. package/packages/web/.next/static/webpack/app/layout.653e365406c0d9ac.hot-update.js +22 -0
  119. package/packages/web/.next/static/webpack/app/layout.6800169a899e3a8b.hot-update.js +22 -0
  120. package/packages/web/.next/static/webpack/app/layout.73c7d02260cc80e4.hot-update.js +22 -0
  121. package/packages/web/.next/static/webpack/app/layout.a2d85d19aa028de1.hot-update.js +22 -0
  122. package/packages/web/.next/static/webpack/app/page.653e365406c0d9ac.hot-update.js +22 -0
  123. package/packages/web/.next/static/webpack/app/page.6800169a899e3a8b.hot-update.js +22 -0
  124. package/packages/web/.next/static/webpack/app/page.73c7d02260cc80e4.hot-update.js +22 -0
  125. package/packages/web/.next/static/webpack/app/page.a2d85d19aa028de1.hot-update.js +22 -0
  126. package/packages/web/.next/static/webpack/{webpack.6fee6306e0f98869.hot-update.js → webpack.16f18baa938a434c.hot-update.js} +2 -2
  127. package/packages/web/.next/static/webpack/{webpack.73e341375c8d429e.hot-update.js → webpack.5fe9fe8578f9c3d2.hot-update.js} +2 -2
  128. package/packages/web/.next/static/webpack/webpack.653e365406c0d9ac.hot-update.js +12 -0
  129. package/packages/web/.next/static/webpack/webpack.6800169a899e3a8b.hot-update.js +12 -0
  130. package/packages/web/.next/static/webpack/webpack.73c7d02260cc80e4.hot-update.js +12 -0
  131. package/packages/web/.next/static/webpack/webpack.a2d85d19aa028de1.hot-update.js +12 -0
  132. package/packages/web/.next/trace +2 -5
  133. package/packages/web/app/globals.css +197 -51
  134. package/packages/web/app/layout.tsx +6 -3
  135. package/packages/web/app/page.tsx +791 -309
  136. package/packages/web/bun.lock +66 -105
  137. package/packages/web/next.config.ts +8 -1
  138. package/packages/web/package.json +5 -2
  139. package/packages/web/postcss.config.mjs +2 -2
  140. package/packages/web/public/apple-icon.png +1 -0
  141. package/packages/web/public/dependency-bottleneck.png +0 -0
  142. package/packages/web/public/icon-dark-32x32.png +1 -0
  143. package/packages/web/public/icon-light-32x32.png +1 -0
  144. package/packages/web/public/icon.svg +1 -0
  145. package/packages/web/public/nextjs-cve-announcement.png +0 -0
  146. package/packages/web/public/phantomraven-npm-attack.png +0 -0
  147. package/packages/web/public/placeholder-logo.png +1 -0
  148. package/packages/web/public/placeholder-logo.svg +1 -0
  149. package/packages/web/public/placeholder-user.jpg +1 -0
  150. package/packages/web/public/placeholder.jpg +1 -0
  151. package/packages/web/public/placeholder.svg +1 -0
  152. package/packages/web/public/react-cve-meme.png +0 -0
  153. package/packages/web/public/wallet-drain-exploit.png +0 -0
  154. package/packages/web/styles/globals.css +125 -0
  155. package/packages/web/.next/server/vendor-chunks/@swc.js +0 -55
  156. package/packages/web/.next/server/vendor-chunks/next.js +0 -3010
  157. package/packages/web/.next/static/chunks/app-pages-internals.js +0 -182
  158. package/packages/web/.next/static/chunks/main-app.js +0 -1882
  159. package/packages/web/.next/static/css/app/layout.css +0 -1237
  160. package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
  161. package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +0 -22
  162. package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +0 -22
  163. package/packages/web/tailwind.config.ts +0 -48
  164. /package/packages/web/.next/static/chunks/{polyfills.js → polyfills-42372ed130431b0a.js} +0 -0
  165. /package/packages/web/.next/static/webpack/{6fee6306e0f98869.webpack.hot-update.json → 653e365406c0d9ac.webpack.hot-update.json} +0 -0
  166. /package/packages/web/.next/static/webpack/{73e341375c8d429e.webpack.hot-update.json → 6800169a899e3a8b.webpack.hot-update.json} +0 -0
@@ -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,13 +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>
109
- <Text> opm install [pkg] Install with on-chain security verification</Text>
108
+ <Text> opm push [--token t] [--otp c] Sign, scan, publish, register + ENS records</Text>
109
+ <Text> opm install [pkg[@ver]] Install with on-chain security verification</Text>
110
+ <Text> opm install pkg@ens.eth Install safest version by ENS author</Text>
110
111
  <Text> opm check Scan all deps: typosquats, CVEs, AI analysis</Text>
111
112
  <Text> opm fix Auto-fix typosquats and vulnerable versions</Text>
112
113
  <Text> opm audit Scan all deps against on-chain security data</Text>
113
- <Text> opm info {'<pkg>'} Show on-chain security info for a package</Text>
114
- <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>
115
116
  <Text> opm whois {'<name>'} Look up an ENS identity on OPM</Text>
116
117
  <Text> </Text>
117
118
  <Text color="cyan" bold>Agent commands:</Text>
@@ -134,7 +135,8 @@ function Help() {
134
135
  <Text> opm pack Create a tarball</Text>
135
136
  <Text> </Text>
136
137
  <Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
137
- <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
+ <Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
138
140
  <Text> </Text>
139
141
  <Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
140
142
  <Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>