opmsec 0.1.0 → 0.1.4

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 (152) hide show
  1. package/.env.example +23 -13
  2. package/.husky/pre-commit +1 -0
  3. package/README.md +256 -173
  4. package/bun.lock +4 -4
  5. package/docs/architecture/agents.mdx +77 -0
  6. package/docs/architecture/benchmarks.mdx +65 -0
  7. package/docs/architecture/overview.mdx +58 -0
  8. package/docs/architecture/scanner.mdx +53 -0
  9. package/docs/cli/audit.mdx +35 -0
  10. package/docs/cli/check.mdx +44 -0
  11. package/docs/cli/fix.mdx +49 -0
  12. package/docs/cli/info.mdx +44 -0
  13. package/docs/cli/install.mdx +71 -0
  14. package/docs/cli/push.mdx +99 -0
  15. package/docs/cli/register-agent.mdx +80 -0
  16. package/docs/cli/view.mdx +52 -0
  17. package/docs/concepts/multi-agent-consensus.mdx +58 -0
  18. package/docs/concepts/on-chain-registry.mdx +74 -0
  19. package/docs/concepts/security-model.mdx +76 -0
  20. package/docs/concepts/zk-agent-verification.mdx +82 -0
  21. package/docs/configuration.mdx +82 -0
  22. package/docs/contract/deployment.mdx +57 -0
  23. package/docs/contract/events.mdx +115 -0
  24. package/docs/contract/functions.mdx +220 -0
  25. package/docs/contract/overview.mdx +58 -0
  26. package/docs/favicon.svg +5 -0
  27. package/docs/introduction.mdx +43 -0
  28. package/docs/logo/dark.svg +5 -0
  29. package/docs/logo/light.svg +5 -0
  30. package/docs/mint.json +106 -0
  31. package/docs/quickstart.mdx +133 -0
  32. package/package.json +7 -6
  33. package/packages/cli/src/commands/author-view.tsx +9 -1
  34. package/packages/cli/src/commands/check.tsx +318 -0
  35. package/packages/cli/src/commands/fix.tsx +294 -0
  36. package/packages/cli/src/commands/install.tsx +501 -47
  37. package/packages/cli/src/commands/push.tsx +53 -22
  38. package/packages/cli/src/commands/register-agent.tsx +227 -0
  39. package/packages/cli/src/components/AgentScores.tsx +20 -6
  40. package/packages/cli/src/components/Hyperlink.tsx +30 -0
  41. package/packages/cli/src/components/ScanReport.tsx +3 -2
  42. package/packages/cli/src/index.tsx +44 -6
  43. package/packages/cli/src/services/avatar.ts +43 -6
  44. package/packages/cli/src/services/chainpatrol.ts +20 -17
  45. package/packages/cli/src/services/contract.ts +41 -8
  46. package/packages/cli/src/services/ens.ts +3 -5
  47. package/packages/cli/src/services/fileverse.ts +12 -13
  48. package/packages/cli/src/services/typosquat.ts +166 -0
  49. package/packages/cli/src/services/version.ts +156 -5
  50. package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
  51. package/packages/contracts/contracts/OPMRegistry.sol +63 -0
  52. package/packages/contracts/scripts/deploy.ts +22 -3
  53. package/packages/core/src/abi.ts +221 -0
  54. package/packages/core/src/benchmarks.ts +450 -0
  55. package/packages/core/src/constants.ts +20 -0
  56. package/packages/core/src/index.ts +2 -0
  57. package/packages/core/src/model-rankings.ts +115 -0
  58. package/packages/core/src/prompt.ts +58 -0
  59. package/packages/core/src/types.ts +41 -0
  60. package/packages/core/src/utils.ts +142 -3
  61. package/packages/scanner/src/agents/base-agent.ts +13 -3
  62. package/packages/scanner/src/index.ts +5 -2
  63. package/packages/scanner/src/queue/memory-queue.ts +8 -3
  64. package/packages/scanner/src/services/benchmark-runner.ts +114 -0
  65. package/packages/scanner/src/services/contract-writer.ts +2 -3
  66. package/packages/scanner/src/services/fileverse.ts +26 -7
  67. package/packages/scanner/src/services/openrouter.ts +61 -4
  68. package/packages/scanner/src/services/report-formatter.ts +122 -3
  69. package/packages/scanner/src/services/zk-verifier.ts +118 -0
  70. package/packages/web/.next/BUILD_ID +1 -0
  71. package/packages/web/.next/app-build-manifest.json +26 -0
  72. package/packages/web/.next/app-path-routes-manifest.json +4 -0
  73. package/packages/web/.next/build-manifest.json +33 -0
  74. package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
  75. package/packages/web/.next/diagnostics/framework.json +1 -0
  76. package/packages/web/.next/export-marker.json +6 -0
  77. package/packages/web/.next/images-manifest.json +58 -0
  78. package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
  79. package/packages/web/.next/next-server.js.nft.json +1 -0
  80. package/packages/web/.next/package.json +1 -0
  81. package/packages/web/.next/prerender-manifest.json +61 -0
  82. package/packages/web/.next/react-loadable-manifest.json +1 -0
  83. package/packages/web/.next/required-server-files.json +320 -0
  84. package/packages/web/.next/routes-manifest.json +53 -0
  85. package/packages/web/.next/server/app/_not-found/page.js +2 -0
  86. package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  87. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  88. package/packages/web/.next/server/app/_not-found.html +1 -0
  89. package/packages/web/.next/server/app/_not-found.meta +8 -0
  90. package/packages/web/.next/server/app/_not-found.rsc +16 -0
  91. package/packages/web/.next/server/app/index.html +1 -0
  92. package/packages/web/.next/server/app/index.meta +7 -0
  93. package/packages/web/.next/server/app/index.rsc +20 -0
  94. package/packages/web/.next/server/app/page.js +2 -0
  95. package/packages/web/.next/server/app/page.js.nft.json +1 -0
  96. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
  97. package/packages/web/.next/server/app-paths-manifest.json +4 -0
  98. package/packages/web/.next/server/chunks/611.js +6 -0
  99. package/packages/web/.next/server/chunks/778.js +30 -0
  100. package/packages/web/.next/server/functions-config-manifest.json +4 -0
  101. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  102. package/packages/web/.next/server/middleware-build-manifest.js +1 -0
  103. package/packages/web/.next/server/middleware-manifest.json +6 -0
  104. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  105. package/packages/web/.next/server/next-font-manifest.js +1 -0
  106. package/packages/web/.next/server/next-font-manifest.json +1 -0
  107. package/packages/web/.next/server/pages/404.html +1 -0
  108. package/packages/web/.next/server/pages/500.html +1 -0
  109. package/packages/web/.next/server/pages/_app.js +1 -0
  110. package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
  111. package/packages/web/.next/server/pages/_document.js +1 -0
  112. package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
  113. package/packages/web/.next/server/pages/_error.js +19 -0
  114. package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
  115. package/packages/web/.next/server/pages-manifest.json +6 -0
  116. package/packages/web/.next/server/server-reference-manifest.js +1 -0
  117. package/packages/web/.next/server/server-reference-manifest.json +1 -0
  118. package/packages/web/.next/server/webpack-runtime.js +1 -0
  119. package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_buildManifest.js +1 -0
  120. package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_ssgManifest.js +1 -0
  121. package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
  122. package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
  123. package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
  124. package/packages/web/.next/static/chunks/app/layout-28a489fb4398663f.js +1 -0
  125. package/packages/web/.next/static/chunks/app/page-e58ccdb78625bce6.js +1 -0
  126. package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
  127. package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
  128. package/packages/web/.next/static/chunks/main-ee293fa6aa18bdd1.js +1 -0
  129. package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  130. package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  131. package/packages/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  132. package/packages/web/.next/static/chunks/webpack-e1ae44446e7f7355.js +1 -0
  133. package/packages/web/.next/static/css/21d69157e271f2ab.css +3 -0
  134. package/packages/web/.next/trace +2 -0
  135. package/packages/web/.next/types/app/layout.ts +84 -0
  136. package/packages/web/.next/types/app/page.ts +84 -0
  137. package/packages/web/.next/types/cache-life.d.ts +141 -0
  138. package/packages/web/.next/types/package.json +1 -0
  139. package/packages/web/.next/types/routes.d.ts +57 -0
  140. package/packages/web/.next/types/validator.ts +61 -0
  141. package/packages/web/app/globals.css +75 -0
  142. package/packages/web/app/layout.tsx +26 -0
  143. package/packages/web/app/page.tsx +361 -0
  144. package/packages/web/bun.lock +300 -0
  145. package/packages/web/next-env.d.ts +6 -0
  146. package/packages/web/next.config.ts +5 -0
  147. package/packages/web/package.json +26 -0
  148. package/packages/web/postcss.config.mjs +8 -0
  149. package/packages/web/public/favicon.svg +5 -0
  150. package/packages/web/public/logo.svg +7 -0
  151. package/packages/web/tailwind.config.ts +48 -0
  152. package/packages/web/tsconfig.json +21 -0
@@ -18,6 +18,26 @@ export const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
18
18
 
19
19
  export const BASE_SEPOLIA_CHAIN_ID = 84532;
20
20
  export const BASE_SEPOLIA_RPC = 'https://sepolia.base.org';
21
+ export const ETH_MAINNET_RPC = 'https://eth.llamarpc.com';
22
+ export const ETH_SEPOLIA_RPC = 'https://ethereum-sepolia-rpc.publicnode.com';
23
+
24
+ export const DEFAULT_CONTRACT_ADDRESS = '0x16684391fc9bf48246B08Afe16d1a57BFa181d48';
25
+
26
+ export const BASE_SEPOLIA_EXPLORER = 'https://sepolia.basescan.org';
27
+
28
+ export function txUrl(hash: string): string {
29
+ return `${BASE_SEPOLIA_EXPLORER}/tx/${hash}`;
30
+ }
31
+
32
+ export function addressUrl(addr: string): string {
33
+ return `${BASE_SEPOLIA_EXPLORER}/address/${addr}`;
34
+ }
35
+
36
+ export function contractUrl(): string {
37
+ return addressUrl(DEFAULT_CONTRACT_ADDRESS);
38
+ }
39
+
40
+ export const FILEVERSE_DEFAULT_URL = 'http://localhost:8001';
21
41
 
22
42
  export const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
23
43
 
@@ -2,4 +2,6 @@ export * from './types';
2
2
  export * from './constants';
3
3
  export * from './utils';
4
4
  export * from './prompt';
5
+ export * from './model-rankings';
6
+ export * from './benchmarks';
5
7
  export { OPM_REGISTRY_ABI } from './abi';
@@ -0,0 +1,115 @@
1
+ const ARTIFICIAL_ANALYSIS_API = 'https://artificialanalysis.ai/api/v2/data/llms/models';
2
+
3
+ export interface ModelRanking {
4
+ id: string;
5
+ name: string;
6
+ slug: string;
7
+ intelligenceIndex: number;
8
+ codingIndex: number;
9
+ }
10
+
11
+ interface AAModelResponse {
12
+ id: string;
13
+ name: string;
14
+ slug: string;
15
+ evaluations?: {
16
+ artificial_analysis_intelligence_index?: number;
17
+ artificial_analysis_coding_index?: number;
18
+ };
19
+ }
20
+
21
+ let cachedRankings: ModelRanking[] | null = null;
22
+ let cacheTimestamp = 0;
23
+ const CACHE_DURATION = 60 * 60 * 1000;
24
+
25
+ export async function fetchModelRankings(): Promise<ModelRanking[]> {
26
+ if (cachedRankings && Date.now() - cacheTimestamp < CACHE_DURATION) {
27
+ return cachedRankings;
28
+ }
29
+
30
+ const apiKey = process.env.ARTIFICIAL_ANALYSIS_API_KEY || '';
31
+ if (!apiKey) return getDefaultRankings();
32
+
33
+ try {
34
+ const res = await fetch(ARTIFICIAL_ANALYSIS_API, {
35
+ headers: { 'x-api-key': apiKey },
36
+ });
37
+ if (!res.ok) throw new Error(`API ${res.status}`);
38
+ const data: { data: AAModelResponse[] } = await res.json();
39
+
40
+ cachedRankings = data.data.map((m) => ({
41
+ id: String(m.id),
42
+ name: m.name,
43
+ slug: m.slug,
44
+ intelligenceIndex: m.evaluations?.artificial_analysis_intelligence_index || 0,
45
+ codingIndex: m.evaluations?.artificial_analysis_coding_index || 0,
46
+ }));
47
+ cacheTimestamp = Date.now();
48
+ return cachedRankings;
49
+ } catch {
50
+ return getDefaultRankings();
51
+ }
52
+ }
53
+
54
+ export function getDefaultRankings(): ModelRanking[] {
55
+ return [
56
+ { id: '1', name: 'Claude Sonnet 4', slug: 'claude-sonnet-4', intelligenceIndex: 55, codingIndex: 52 },
57
+ { id: '2', name: 'GPT-4.1', slug: 'gpt-4.1', intelligenceIndex: 50, codingIndex: 48 },
58
+ { id: '3', name: 'Gemini 2.5 Flash', slug: 'gemini-2.5-flash', intelligenceIndex: 52, codingIndex: 45 },
59
+ { id: '4', name: 'DeepSeek Chat', slug: 'deepseek-chat', intelligenceIndex: 42, codingIndex: 40 },
60
+ { id: '5', name: 'GPT-4.1-mini', slug: 'gpt-4.1-mini', intelligenceIndex: 40, codingIndex: 38 },
61
+ { id: '6', name: 'GPT-4.1-nano', slug: 'gpt-4.1-nano', intelligenceIndex: 35, codingIndex: 32 },
62
+ ];
63
+ }
64
+
65
+ const MODEL_SLUGS: Record<string, string> = {
66
+ 'anthropic/claude-sonnet-4-20250514': 'claude-sonnet-4',
67
+ 'anthropic/claude-sonnet-4': 'claude-sonnet-4',
68
+ 'google/gemini-2.5-flash': 'gemini-2.5-flash',
69
+ 'deepseek/deepseek-chat': 'deepseek-chat',
70
+ 'openai/gpt-4.1': 'gpt-4.1',
71
+ 'gpt-4.1': 'gpt-4.1',
72
+ 'openai/gpt-4.1-mini': 'gpt-4.1-mini',
73
+ 'gpt-4.1-mini': 'gpt-4.1-mini',
74
+ 'openai/gpt-4.1-nano': 'gpt-4.1-nano',
75
+ 'gpt-4.1-nano': 'gpt-4.1-nano',
76
+ };
77
+
78
+ function findModel(rankings: ModelRanking[], modelSlug: string): ModelRanking | undefined {
79
+ const normalizedSlug = MODEL_SLUGS[modelSlug] || modelSlug.toLowerCase();
80
+ return rankings.find(m => m.slug.toLowerCase() === normalizedSlug)
81
+ || rankings.find(m => m.name.toLowerCase() === normalizedSlug)
82
+ || rankings.find(m => m.name.toLowerCase().includes(normalizedSlug));
83
+ }
84
+
85
+ export async function getModelWeight(modelSlug: string): Promise<number> {
86
+ const model = findModel(await fetchModelRankings(), modelSlug);
87
+ if (!model) return 50;
88
+ return Math.round((model.intelligenceIndex + model.codingIndex) / 2);
89
+ }
90
+
91
+ export async function getModelIntelligence(modelSlug: string): Promise<number> {
92
+ const model = findModel(await fetchModelRankings(), modelSlug);
93
+ return model?.intelligenceIndex || 50;
94
+ }
95
+
96
+ export async function getModelRankingFor(modelSlug: string): Promise<{ intelligence: number; coding: number; weight: number }> {
97
+ const model = findModel(await fetchModelRankings(), modelSlug);
98
+ const intelligence = model?.intelligenceIndex || 50;
99
+ const coding = model?.codingIndex || 50;
100
+ return { intelligence, coding, weight: Math.round((intelligence + coding) / 2) };
101
+ }
102
+
103
+ export function calculateWeightedScore(
104
+ scores: { score: number; weight: number }[]
105
+ ): number {
106
+ if (scores.length === 0) return 0;
107
+
108
+ const totalWeight = scores.reduce((sum, s) => sum + s.weight, 0);
109
+ if (totalWeight === 0) {
110
+ return Math.round(scores.reduce((sum, s) => sum + s.score, 0) / scores.length);
111
+ }
112
+
113
+ const weightedSum = scores.reduce((sum, s) => sum + s.score * s.weight, 0);
114
+ return Math.round(weightedSum / totalWeight);
115
+ }
@@ -109,3 +109,61 @@ ${codeStr || 'No source files found.'}
109
109
 
110
110
  Analyze this package thoroughly and respond with the JSON schema specified in your system instructions.`;
111
111
  }
112
+
113
+ export const CHECK_SYSTEM_PROMPT = `You are a dependency security auditor. You analyze a project's full dependency list for typosquatting, supply chain risks, and suspicious patterns.
114
+
115
+ You MUST respond with a valid JSON object matching this exact schema -- no markdown, no explanation outside the JSON:
116
+
117
+ {
118
+ "findings": [
119
+ {
120
+ "package": "<string: package name>",
121
+ "version": "<string: version>",
122
+ "issue": "<typosquat | malicious_pattern | suspicious_metadata | dependency_confusion | safe>",
123
+ "severity": "<CRITICAL | HIGH | MEDIUM | LOW | NONE>",
124
+ "explanation": "<string: why this is flagged>",
125
+ "suggested_replacement": "<string | null: correct package name if typosquat, or null>",
126
+ "suggested_version": "<string | null: safer version if applicable, or null>"
127
+ }
128
+ ],
129
+ "overall_assessment": "<string: 2-3 sentence summary of the dependency tree health>",
130
+ "risk_score": <number 0-100>
131
+ }
132
+
133
+ Focus on:
134
+ 1. TYPOSQUATTING: Names suspiciously similar to popular packages (missing/extra/swapped chars, separator tricks like _ vs -)
135
+ 2. MALICIOUS PATTERNS: Known malicious package names, suspicious scopes, exfiltration-oriented package descriptions
136
+ 3. DEPENDENCY CONFUSION: Public packages that shadow internal/scoped packages
137
+ 4. SUSPICIOUS METADATA: Very new packages with no downloads claiming to be utilities, packages with copy-pasted descriptions from popular packages
138
+ 5. VERSION RISKS: Packages pinned to pre-release or yanked versions
139
+
140
+ Only flag packages you have genuine concern about. Do not flag well-known legitimate packages.`;
141
+
142
+ export interface DepEntry {
143
+ name: string;
144
+ version: string;
145
+ downloads?: number;
146
+ description?: string;
147
+ author?: string;
148
+ created?: string;
149
+ }
150
+
151
+ export function buildCheckPrompt(deps: DepEntry[], devDeps: DepEntry[]): string {
152
+ const fmtDep = (d: DepEntry) => {
153
+ const meta = [d.downloads !== undefined ? `downloads: ${d.downloads}/wk` : ''];
154
+ if (d.description) meta.push(`desc: "${d.description}"`);
155
+ if (d.author) meta.push(`author: ${d.author}`);
156
+ if (d.created) meta.push(`created: ${d.created}`);
157
+ return `- ${d.name}@${d.version} (${meta.filter(Boolean).join(', ')})`;
158
+ };
159
+
160
+ return `Analyze this project's dependencies for security risks.
161
+
162
+ ## Dependencies (${deps.length})
163
+ ${deps.map(fmtDep).join('\n') || 'none'}
164
+
165
+ ## Dev Dependencies (${devDeps.length})
166
+ ${devDeps.map(fmtDep).join('\n') || 'none'}
167
+
168
+ Analyze each dependency and respond with the JSON schema from your system instructions. Flag any typosquatting, suspicious packages, or risky patterns.`;
169
+ }
@@ -42,6 +42,10 @@ export interface AgentScanResult {
42
42
  export interface AgentEntry {
43
43
  agent_id: string;
44
44
  model: string;
45
+ model_intelligence?: number;
46
+ model_coding?: number;
47
+ model_weight?: number;
48
+ score_tx_hash?: string;
45
49
  result: AgentScanResult;
46
50
  }
47
51
 
@@ -102,3 +106,40 @@ export interface ChainPatrolResult {
102
106
  status: 'UNKNOWN' | 'ALLOWED' | 'BLOCKED';
103
107
  source: string;
104
108
  }
109
+
110
+ export interface CheckDepResult {
111
+ name: string;
112
+ version: string;
113
+ typosquat: { likelyTarget: string; confidence: string; reason: string } | null;
114
+ cveCount: number;
115
+ cveCritical: number;
116
+ cveHigh: number;
117
+ cveIds: string[];
118
+ fixVersion: string | null;
119
+ onChainScore: number | null;
120
+ }
121
+
122
+ export interface CheckAgentResult {
123
+ agentId: string;
124
+ model: string;
125
+ intelligence: number;
126
+ coding: number;
127
+ findings: Array<{
128
+ package: string;
129
+ issue: string;
130
+ severity: string;
131
+ explanation: string;
132
+ suggested_replacement: string | null;
133
+ suggested_version: string | null;
134
+ }>;
135
+ overall: string;
136
+ riskScore: number;
137
+ }
138
+
139
+ export interface CheckReport {
140
+ project: string;
141
+ timestamp: string;
142
+ totalDeps: number;
143
+ deps: CheckDepResult[];
144
+ agents: CheckAgentResult[];
145
+ }
@@ -17,10 +17,14 @@ export function truncateAddress(addr: string): string {
17
17
  return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
18
18
  }
19
19
 
20
- export function getEnvOrThrow(key: string): string {
20
+ export function getEnvOrThrow(key: string, ...fallbackKeys: string[]): string {
21
21
  const val = process.env[key];
22
- if (!val) throw new Error(`Missing required env var: ${key}`);
23
- return val;
22
+ if (val) return val;
23
+ for (const fk of fallbackKeys) {
24
+ const fv = process.env[fk];
25
+ if (fv) return fv;
26
+ }
27
+ throw new Error(`Missing required env var: ${key}`);
24
28
  }
25
29
 
26
30
  export function getEnvOrDefault(key: string, fallback: string): string {
@@ -41,6 +45,141 @@ export function validateScanResult(obj: unknown): obj is AgentScanResult {
41
45
  );
42
46
  }
43
47
 
48
+ const VALID_RISK_LEVELS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const;
49
+ const VALID_RECOMMENDATIONS = ['SAFE', 'CAUTION', 'WARN', 'BLOCK'] as const;
50
+ const SCORE_KEYS = ['risk_score', 'score', 'riskScore', 'risk_rating'];
51
+ const LEVEL_KEYS = ['risk_level', 'riskLevel', 'level', 'severity', 'verdict', 'rating'];
52
+ const TEXT_KEYS = ['reasoning', 'summary', 'explanation', 'description', 'analysis', 'one_line_summary', 'one_liner'];
53
+
54
+ function deepFind(obj: Record<string, any>, keys: string[], type: 'number' | 'string', depth = 0): any {
55
+ if (depth > 4 || !obj || typeof obj !== 'object') return undefined;
56
+ for (const key of keys) {
57
+ const val = obj[key];
58
+ if (val !== undefined && val !== null) {
59
+ if (type === 'number') {
60
+ if (typeof val === 'number') return val;
61
+ if (typeof val === 'string' && !isNaN(parseFloat(val))) return parseFloat(val);
62
+ } else if (type === 'string' && typeof val === 'string' && val.length > 0) {
63
+ return val;
64
+ }
65
+ }
66
+ }
67
+ for (const key of Object.keys(obj)) {
68
+ const val = obj[key];
69
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
70
+ const found = deepFind(val, keys, type, depth + 1);
71
+ if (found !== undefined) return found;
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ function deepFindArray(obj: Record<string, any>, keys: string[], depth = 0): any[] | undefined {
78
+ if (depth > 4 || !obj || typeof obj !== 'object') return undefined;
79
+ for (const key of keys) {
80
+ if (Array.isArray(obj[key])) return obj[key];
81
+ }
82
+ for (const key of Object.keys(obj)) {
83
+ const val = obj[key];
84
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
85
+ const found = deepFindArray(val, keys, depth + 1);
86
+ if (found) return found;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ function deepFindObj(obj: Record<string, any>, keys: string[], depth = 0): Record<string, any> | undefined {
93
+ if (depth > 3 || !obj || typeof obj !== 'object') return undefined;
94
+ for (const key of keys) {
95
+ const val = obj[key];
96
+ if (val && typeof val === 'object' && !Array.isArray(val)) return val;
97
+ }
98
+ return undefined;
99
+ }
100
+
101
+ function normalizeRiskLevel(val: unknown): RiskLevel {
102
+ if (typeof val !== 'string') return 'MEDIUM';
103
+ const upper = val.toUpperCase().trim();
104
+ if (VALID_RISK_LEVELS.includes(upper as RiskLevel)) return upper as RiskLevel;
105
+ if (upper === 'SAFE' || upper === 'NONE' || upper === 'INFO') return 'LOW';
106
+ if (upper === 'MODERATE' || upper === 'SUSPICIOUS') return 'MEDIUM';
107
+ if (upper === 'DANGEROUS' || upper === 'SEVERE') return 'CRITICAL';
108
+ return 'MEDIUM';
109
+ }
110
+
111
+ function normalizeRecommendation(val: unknown, riskLevel: RiskLevel): string {
112
+ if (typeof val === 'string') {
113
+ const upper = val.toUpperCase().trim();
114
+ if (VALID_RECOMMENDATIONS.includes(upper as any)) return upper;
115
+ }
116
+ const map: Record<RiskLevel, string> = { LOW: 'SAFE', MEDIUM: 'CAUTION', HIGH: 'WARN', CRITICAL: 'BLOCK' };
117
+ return map[riskLevel] || 'CAUTION';
118
+ }
119
+
120
+ /**
121
+ * Recursively searches an arbitrarily-shaped LLM response for risk_score,
122
+ * risk_level, reasoning, etc. and assembles a valid AgentScanResult.
123
+ */
124
+ export function normalizeScanResult(raw: unknown): AgentScanResult | null {
125
+ if (!raw || typeof raw !== 'object') return null;
126
+ const o = raw as Record<string, any>;
127
+
128
+ const riskScore = deepFind(o, SCORE_KEYS, 'number');
129
+ if (riskScore === undefined || isNaN(riskScore)) return null;
130
+
131
+ const rawLevel = deepFind(o, LEVEL_KEYS, 'string');
132
+ const riskLevel = normalizeRiskLevel(rawLevel);
133
+
134
+ const reasoning = deepFind(o, TEXT_KEYS, 'string') ?? `Risk score: ${riskScore}`;
135
+
136
+ const rawVulns = deepFindArray(o, ['vulnerabilities', 'findings', 'issues', 'alerts', 'concerns']);
137
+ const vulnerabilities = rawVulns
138
+ ? rawVulns.map((f: any) => ({
139
+ severity: normalizeRiskLevel(f.severity ?? f.level ?? f.risk_level),
140
+ category: f.category || f.type || f.issue || 'unknown',
141
+ description: f.description || f.message || f.detail || f.title || '',
142
+ file: f.file || f.location || f.path || '',
143
+ evidence: f.evidence || f.code || f.snippet || '',
144
+ }))
145
+ : [];
146
+
147
+ const sci = deepFindObj(o, ['supply_chain_indicators', 'supplyChainIndicators', 'indicators']);
148
+ const supply_chain_indicators = sci ?? {
149
+ has_install_scripts: false,
150
+ has_native_bindings: false,
151
+ has_obfuscated_code: false,
152
+ has_network_calls: false,
153
+ has_filesystem_access: false,
154
+ has_process_spawn: false,
155
+ has_eval_usage: false,
156
+ accesses_env_variables: false,
157
+ };
158
+
159
+ const va = deepFindObj(o, ['version_analysis', 'versionAnalysis']);
160
+ const version_analysis = va ?? {
161
+ version_reviewed: deepFind(o, ['version', 'version_reviewed'], 'string') ?? '',
162
+ previous_versions_reviewed: [],
163
+ changelog_risk: 'NONE',
164
+ changelog_reasoning: '',
165
+ };
166
+
167
+ const recommendation = normalizeRecommendation(
168
+ deepFind(o, ['recommendation', 'action', 'verdict'], 'string'),
169
+ riskLevel,
170
+ );
171
+
172
+ return {
173
+ risk_score: Math.max(0, Math.min(100, Math.round(riskScore))),
174
+ risk_level: riskLevel,
175
+ reasoning,
176
+ vulnerabilities,
177
+ supply_chain_indicators,
178
+ version_analysis,
179
+ recommendation: recommendation as any,
180
+ };
181
+ }
182
+
44
183
  export function safeJsonParse<T>(raw: string): T | null {
45
184
  try {
46
185
  return JSON.parse(raw) as T;
@@ -1,4 +1,4 @@
1
- import { SYSTEM_PROMPT, buildUserPrompt } from '@opm/core';
1
+ import { SYSTEM_PROMPT, buildUserPrompt, getModelRankingFor } from '@opm/core';
2
2
  import type { AgentEntry, KnownCVE } from '@opm/core';
3
3
  import {
4
4
  fetchPackageData, buildLocalPackageData, extractMetadata,
@@ -59,10 +59,16 @@ export async function runAgent(
59
59
  const userPrompt = buildUserPrompt(meta, history, sourceFiles, knownCVEs);
60
60
  const result = await callLLM(config.model, SYSTEM_PROMPT, userPrompt);
61
61
 
62
+ log(`[${config.agentId}] Fetching model intelligence ranking...`);
63
+ const { intelligence, coding, weight } = await getModelRankingFor(config.model);
64
+
65
+ log(`[${config.agentId}] ${config.model} — intelligence: ${intelligence}, coding: ${coding}, weight: ${weight}`);
66
+
62
67
  log(`[${config.agentId}] Submitting score (${result.risk_score}) to contract...`);
68
+ let scoreTxHash: string | undefined;
63
69
  try {
64
- await submitScoreOnChain(packageName, version, result.risk_score, result.reasoning);
65
- log(`[${config.agentId}] Score submitted on-chain`);
70
+ scoreTxHash = await submitScoreOnChain(packageName, version, result.risk_score, result.reasoning);
71
+ log(`[${config.agentId}] Score submitted on-chain ✓`);
66
72
  } catch (err: any) {
67
73
  log(`[${config.agentId}] On-chain: ${err?.shortMessage || err?.message || 'failed'}`);
68
74
  }
@@ -70,6 +76,10 @@ export async function runAgent(
70
76
  return {
71
77
  agent_id: config.agentId,
72
78
  model: config.model,
79
+ model_intelligence: intelligence,
80
+ model_coding: coding,
81
+ model_weight: weight,
82
+ score_tx_hash: scoreTxHash,
73
83
  result,
74
84
  };
75
85
  }
@@ -4,10 +4,13 @@ export { enqueueScan } from './queue/memory-queue';
4
4
  export type { LocalScanContext } from './agents/base-agent';
5
5
  export { runAgent } from './agents/base-agent';
6
6
  export { getAgentConfigs } from './agents/agent-configs';
7
- export { callLLM, getLLMProvider } from './services/openrouter';
7
+ export { callLLM, callLLMRaw, getLLMProvider } from './services/openrouter';
8
8
  export { fetchPackageData, extractMetadata, buildVersionHistory, fetchSourceFiles, extractLocalSourceFiles, buildLocalPackageData } from './services/npm-registry';
9
9
  export { submitScoreOnChain, setReportURIOnChain } from './services/contract-writer';
10
- export { uploadReportToFileverse, fetchReportFromFileverse } from './services/fileverse';
10
+ export { uploadReportToFileverse, uploadCheckReportToFileverse, fetchReportFromFileverse } from './services/fileverse';
11
+ export { formatCheckReportAsMarkdown } from './services/report-formatter';
12
+ export { runBenchmarkSuite, type AgentCandidate, type BenchmarkRunResult } from './services/benchmark-runner';
13
+ export { generateProof, verifyProof, generateCommitment, proofToOnChainBytes, type ZKProof } from './services/zk-verifier';
11
14
 
12
15
  if (import.meta.main) {
13
16
  const [pkg, ver] = process.argv.slice(2);
@@ -1,5 +1,5 @@
1
1
  import type { ScanReport, AgentEntry } from '@opm/core';
2
- import { averageScores, classifyRisk } from '@opm/core';
2
+ import { classifyRisk, getModelWeight, calculateWeightedScore } from '@opm/core';
3
3
  import { runAgent, type LocalScanContext } from '../agents/base-agent';
4
4
  import { getAgentConfigs } from '../agents/agent-configs';
5
5
  import { setReportURIOnChain } from '../services/contract-writer';
@@ -57,8 +57,13 @@ async function executeScan(
57
57
 
58
58
  if (agents.length === 0) throw new Error('All agents failed');
59
59
 
60
- const scores = agents.map((a) => a.result.risk_score);
61
- const aggScore = averageScores(scores);
60
+ const weights = await Promise.all(agents.map(a => getModelWeight(a.model)));
61
+ const weightedScores = agents.map((a, i) => ({
62
+ score: a.result.risk_score,
63
+ weight: weights[i],
64
+ }));
65
+
66
+ const aggScore = calculateWeightedScore(weightedScores);
62
67
 
63
68
  const report: ScanReport = {
64
69
  package: packageName,
@@ -0,0 +1,114 @@
1
+ import {
2
+ generateBenchmarkDataset,
3
+ buildBenchmarkPrompt,
4
+ evaluateBenchmark,
5
+ SYSTEM_PROMPT,
6
+ type BenchmarkCase,
7
+ type BenchmarkResult,
8
+ } from '@opm/core';
9
+ import { callLLM } from './openrouter';
10
+ import {
11
+ generateCommitment,
12
+ generateProof,
13
+ verifyProof,
14
+ type ZKProof,
15
+ } from './zk-verifier';
16
+
17
+ export interface AgentCandidate {
18
+ name: string;
19
+ model: string;
20
+ systemPrompt?: string;
21
+ }
22
+
23
+ export interface BenchmarkRunResult {
24
+ candidate: AgentCandidate;
25
+ results: BenchmarkResult[];
26
+ passed: number;
27
+ failed: number;
28
+ total: number;
29
+ accuracyPct: number;
30
+ zkProof: ZKProof;
31
+ verified: boolean;
32
+ failureReasons: string[];
33
+ }
34
+
35
+ export async function runBenchmarkSuite(
36
+ candidate: AgentCandidate,
37
+ onStatus?: (msg: string) => void,
38
+ ): Promise<BenchmarkRunResult> {
39
+ const log = onStatus || console.log;
40
+ const benchmarks = generateBenchmarkDataset();
41
+ const systemPrompt = candidate.systemPrompt || SYSTEM_PROMPT;
42
+
43
+ log(`Generating benchmark commitment for ${benchmarks.length} test cases...`);
44
+
45
+ const expectedVerdicts = benchmarks.map((b) => {
46
+ const levelMap: Record<string, number> = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
47
+ return levelMap[b.expected.risk_level] ?? 0;
48
+ });
49
+
50
+ const commitment = generateCommitment(expectedVerdicts);
51
+ log(`Commitment generated: ${commitment.expectedHash.slice(0, 16)}...`);
52
+
53
+ const results: BenchmarkResult[] = [];
54
+ const actualVerdicts: number[] = [];
55
+
56
+ for (let i = 0; i < benchmarks.length; i++) {
57
+ const bench = benchmarks[i];
58
+ log(`[${i + 1}/${benchmarks.length}] ${bench.description}...`);
59
+
60
+ try {
61
+ const userPrompt = buildBenchmarkPrompt(bench);
62
+ const agentResult = await callLLM(candidate.model, systemPrompt, userPrompt);
63
+
64
+ const evaluation = evaluateBenchmark(bench, agentResult.risk_level, agentResult.risk_score);
65
+ results.push(evaluation);
66
+
67
+ const levelMap: Record<string, number> = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
68
+ actualVerdicts.push(evaluation.verdict === 'PASS'
69
+ ? (levelMap[bench.expected.risk_level] ?? 0)
70
+ : (levelMap[agentResult.risk_level] ?? 0));
71
+
72
+ const icon = evaluation.verdict === 'PASS' ? '✓' : '✗';
73
+ log(`[${i + 1}/${benchmarks.length}] ${icon} ${bench.category}: score=${agentResult.risk_score} level=${agentResult.risk_level} (expected ${bench.expected.risk_level})`);
74
+ } catch (err: any) {
75
+ log(`[${i + 1}/${benchmarks.length}] ✗ Error: ${err?.message || 'failed'}`);
76
+ results.push({
77
+ caseId: bench.id,
78
+ category: bench.category,
79
+ expectedLevel: bench.expected.risk_level,
80
+ actualLevel: 'ERROR',
81
+ expectedScoreRange: [bench.expected.min_risk_score, bench.expected.max_risk_score],
82
+ actualScore: -1,
83
+ verdict: 'FAIL',
84
+ reason: `Agent error: ${err?.message || 'unknown'}`,
85
+ });
86
+ actualVerdicts.push(-1);
87
+ }
88
+ }
89
+
90
+ log('Generating ZK proof of accuracy...');
91
+ const zkProof = generateProof(commitment, expectedVerdicts, actualVerdicts);
92
+ const verified = verifyProof(zkProof);
93
+
94
+ const passed = results.filter((r) => r.verdict === 'PASS').length;
95
+ const failed = results.filter((r) => r.verdict === 'FAIL').length;
96
+ const failureReasons = results
97
+ .filter((r) => r.verdict === 'FAIL')
98
+ .map((r) => `${r.caseId} (${r.category}): ${r.reason}`);
99
+
100
+ log(`ZK proof ${verified ? 'verified ✓' : 'INVALID ✗'}`);
101
+ log(`Accuracy: ${passed}/${results.length} (${Math.round((passed / results.length) * 100)}%)`);
102
+
103
+ return {
104
+ candidate,
105
+ results,
106
+ passed,
107
+ failed,
108
+ total: results.length,
109
+ accuracyPct: Math.round((passed / results.length) * 100),
110
+ zkProof,
111
+ verified,
112
+ failureReasons,
113
+ };
114
+ }
@@ -1,12 +1,11 @@
1
1
  import { ethers } from 'ethers';
2
- import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC } from '@opm/core';
2
+ import { OPM_REGISTRY_ABI, getEnvOrThrow, getEnvOrDefault, BASE_SEPOLIA_RPC, DEFAULT_CONTRACT_ADDRESS } from '@opm/core';
3
3
 
4
4
  function getContract() {
5
5
  const rpc = getEnvOrDefault('BASE_SEPOLIA_RPC_URL', BASE_SEPOLIA_RPC);
6
6
  const provider = new ethers.JsonRpcProvider(rpc);
7
7
  const wallet = new ethers.Wallet(getEnvOrThrow('AGENT_PRIVATE_KEY'), provider);
8
- const address = getEnvOrThrow('CONTRACT_ADDRESS');
9
- return new ethers.Contract(address, OPM_REGISTRY_ABI, wallet);
8
+ return new ethers.Contract(getEnvOrDefault('CONTRACT_ADDRESS', DEFAULT_CONTRACT_ADDRESS), OPM_REGISTRY_ABI, wallet);
10
9
  }
11
10
 
12
11
  export async function submitScoreOnChain(