opmsec 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.env.example +23 -13
  2. package/README.md +256 -173
  3. package/docs/architecture/agents.mdx +77 -0
  4. package/docs/architecture/benchmarks.mdx +65 -0
  5. package/docs/architecture/overview.mdx +58 -0
  6. package/docs/architecture/scanner.mdx +53 -0
  7. package/docs/cli/audit.mdx +35 -0
  8. package/docs/cli/check.mdx +44 -0
  9. package/docs/cli/fix.mdx +49 -0
  10. package/docs/cli/info.mdx +44 -0
  11. package/docs/cli/install.mdx +71 -0
  12. package/docs/cli/push.mdx +99 -0
  13. package/docs/cli/register-agent.mdx +80 -0
  14. package/docs/cli/view.mdx +52 -0
  15. package/docs/concepts/multi-agent-consensus.mdx +58 -0
  16. package/docs/concepts/on-chain-registry.mdx +74 -0
  17. package/docs/concepts/security-model.mdx +76 -0
  18. package/docs/concepts/zk-agent-verification.mdx +82 -0
  19. package/docs/configuration.mdx +82 -0
  20. package/docs/contract/deployment.mdx +57 -0
  21. package/docs/contract/events.mdx +115 -0
  22. package/docs/contract/functions.mdx +220 -0
  23. package/docs/contract/overview.mdx +58 -0
  24. package/docs/favicon.svg +5 -0
  25. package/docs/introduction.mdx +43 -0
  26. package/docs/logo/dark.svg +5 -0
  27. package/docs/logo/light.svg +5 -0
  28. package/docs/mint.json +106 -0
  29. package/docs/quickstart.mdx +133 -0
  30. package/package.json +3 -3
  31. package/packages/cli/src/commands/author-view.tsx +9 -1
  32. package/packages/cli/src/commands/check.tsx +318 -0
  33. package/packages/cli/src/commands/fix.tsx +294 -0
  34. package/packages/cli/src/commands/install.tsx +229 -33
  35. package/packages/cli/src/commands/push.tsx +53 -22
  36. package/packages/cli/src/commands/register-agent.tsx +227 -0
  37. package/packages/cli/src/components/AgentScores.tsx +20 -6
  38. package/packages/cli/src/components/Hyperlink.tsx +30 -0
  39. package/packages/cli/src/components/ScanReport.tsx +3 -2
  40. package/packages/cli/src/index.tsx +41 -5
  41. package/packages/cli/src/services/avatar.ts +43 -6
  42. package/packages/cli/src/services/chainpatrol.ts +20 -17
  43. package/packages/cli/src/services/contract.ts +41 -8
  44. package/packages/cli/src/services/ens.ts +3 -5
  45. package/packages/cli/src/services/fileverse.ts +12 -13
  46. package/packages/cli/src/services/typosquat.ts +166 -0
  47. package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
  48. package/packages/contracts/contracts/OPMRegistry.sol +63 -0
  49. package/packages/contracts/scripts/deploy.ts +22 -3
  50. package/packages/core/src/abi.ts +221 -0
  51. package/packages/core/src/benchmarks.ts +450 -0
  52. package/packages/core/src/constants.ts +20 -0
  53. package/packages/core/src/index.ts +2 -0
  54. package/packages/core/src/model-rankings.ts +115 -0
  55. package/packages/core/src/prompt.ts +58 -0
  56. package/packages/core/src/types.ts +41 -0
  57. package/packages/core/src/utils.ts +7 -3
  58. package/packages/scanner/src/agents/base-agent.ts +13 -3
  59. package/packages/scanner/src/index.ts +5 -2
  60. package/packages/scanner/src/queue/memory-queue.ts +8 -3
  61. package/packages/scanner/src/services/benchmark-runner.ts +114 -0
  62. package/packages/scanner/src/services/contract-writer.ts +2 -3
  63. package/packages/scanner/src/services/fileverse.ts +26 -7
  64. package/packages/scanner/src/services/openrouter.ts +46 -0
  65. package/packages/scanner/src/services/report-formatter.ts +122 -3
  66. package/packages/scanner/src/services/zk-verifier.ts +118 -0
  67. package/packages/web/.next/app-build-manifest.json +15 -0
  68. package/packages/web/.next/build-manifest.json +20 -0
  69. package/packages/web/.next/package.json +1 -0
  70. package/packages/web/.next/prerender-manifest.json +11 -0
  71. package/packages/web/.next/react-loadable-manifest.json +1 -0
  72. package/packages/web/.next/routes-manifest.json +1 -0
  73. package/packages/web/.next/server/app/page.js +272 -0
  74. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
  75. package/packages/web/.next/server/app-paths-manifest.json +3 -0
  76. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  77. package/packages/web/.next/server/middleware-build-manifest.js +22 -0
  78. package/packages/web/.next/server/middleware-manifest.json +6 -0
  79. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  80. package/packages/web/.next/server/next-font-manifest.js +1 -0
  81. package/packages/web/.next/server/next-font-manifest.json +1 -0
  82. package/packages/web/.next/server/pages-manifest.json +1 -0
  83. package/packages/web/.next/server/server-reference-manifest.js +1 -0
  84. package/packages/web/.next/server/server-reference-manifest.json +5 -0
  85. package/packages/web/.next/server/vendor-chunks/@swc.js +55 -0
  86. package/packages/web/.next/server/vendor-chunks/next.js +3010 -0
  87. package/packages/web/.next/server/webpack-runtime.js +209 -0
  88. package/packages/web/.next/static/chunks/app/layout.js +39 -0
  89. package/packages/web/.next/static/chunks/app/page.js +61 -0
  90. package/packages/web/.next/static/chunks/app-pages-internals.js +182 -0
  91. package/packages/web/.next/static/chunks/main-app.js +1882 -0
  92. package/packages/web/.next/static/chunks/polyfills.js +1 -0
  93. package/packages/web/.next/static/chunks/webpack.js +1393 -0
  94. package/packages/web/.next/static/css/app/layout.css +1237 -0
  95. package/packages/web/.next/static/development/_buildManifest.js +1 -0
  96. package/packages/web/.next/static/development/_ssgManifest.js +1 -0
  97. package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
  98. package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +1 -0
  99. package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +1 -0
  100. package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +22 -0
  101. package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +22 -0
  102. package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +22 -0
  103. package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +22 -0
  104. package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +12 -0
  105. package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +12 -0
  106. package/packages/web/.next/trace +5 -0
  107. package/packages/web/.next/types/app/layout.ts +84 -0
  108. package/packages/web/.next/types/app/page.ts +84 -0
  109. package/packages/web/.next/types/cache-life.d.ts +141 -0
  110. package/packages/web/.next/types/package.json +1 -0
  111. package/packages/web/.next/types/routes.d.ts +57 -0
  112. package/packages/web/.next/types/validator.ts +61 -0
  113. package/packages/web/app/globals.css +75 -0
  114. package/packages/web/app/layout.tsx +26 -0
  115. package/packages/web/app/page.tsx +358 -0
  116. package/packages/web/bun.lock +300 -0
  117. package/packages/web/next-env.d.ts +6 -0
  118. package/packages/web/next.config.ts +5 -0
  119. package/packages/web/package.json +26 -0
  120. package/packages/web/postcss.config.mjs +8 -0
  121. package/packages/web/public/favicon.svg +5 -0
  122. package/packages/web/public/logo.svg +7 -0
  123. package/packages/web/tailwind.config.ts +48 -0
  124. package/packages/web/tsconfig.json +21 -0
@@ -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 {
@@ -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(
@@ -1,13 +1,11 @@
1
- import type { ScanReport } from '@opm/core';
2
- import { getEnvOrDefault } from '@opm/core';
3
- import { formatReportAsMarkdown } from './report-formatter';
4
-
5
- const DEFAULT_API_URL = 'http://localhost:8001';
1
+ import type { ScanReport, CheckReport } from '@opm/core';
2
+ import { getEnvOrDefault, FILEVERSE_DEFAULT_URL } from '@opm/core';
3
+ import { formatReportAsMarkdown, formatCheckReportAsMarkdown } from './report-formatter';
6
4
  const POLL_INTERVAL_MS = 3000;
7
5
  const POLL_TIMEOUT_MS = 60_000;
8
6
 
9
7
  function getApiConfig() {
10
- const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', DEFAULT_API_URL);
8
+ const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', FILEVERSE_DEFAULT_URL);
11
9
  const apiKey = process.env.FILEVERSE_API_KEY;
12
10
  if (!apiKey) throw new Error('FILEVERSE_API_KEY is required (generate at ddocs.new → Settings → Developer Mode)');
13
11
  return { apiUrl, apiKey };
@@ -56,11 +54,32 @@ async function pollForSync(apiUrl: string, apiKey: string, ddocId: string): Prom
56
54
  return `https://ddocs.new/pending/${ddocId}`;
57
55
  }
58
56
 
57
+ export async function uploadCheckReportToFileverse(report: CheckReport): Promise<string> {
58
+ const { apiUrl, apiKey } = getApiConfig();
59
+ const title = `OPM Check Report: ${report.project} (${report.totalDeps} deps)`;
60
+ const content = formatCheckReportAsMarkdown(report);
61
+
62
+ const res = await fetch(`${apiUrl}/api/ddocs?apiKey=${encodeURIComponent(apiKey)}`, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json' },
65
+ body: JSON.stringify({ title, content }),
66
+ });
67
+
68
+ if (!res.ok) {
69
+ const body = await res.text();
70
+ throw new Error(`Fileverse create failed (${res.status}): ${body}`);
71
+ }
72
+
73
+ const { data } = await res.json() as { data: { ddocId: string; syncStatus: string; link?: string } };
74
+ if (data.syncStatus === 'synced' && data.link) return data.link;
75
+ return pollForSync(apiUrl, apiKey, data.ddocId);
76
+ }
77
+
59
78
  export async function fetchReportFromFileverse(reportURI: string): Promise<ScanReport | null> {
60
79
  if (!reportURI || reportURI.startsWith('local://')) return null;
61
80
 
62
81
  const apiKey = process.env.FILEVERSE_API_KEY;
63
- const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', DEFAULT_API_URL);
82
+ const apiUrl = getEnvOrDefault('FILEVERSE_API_URL', FILEVERSE_DEFAULT_URL);
64
83
 
65
84
  const ddocId = extractDdocId(reportURI);
66
85
  if (ddocId && apiKey) {
@@ -84,3 +84,49 @@ export async function callLLM(
84
84
 
85
85
  return parsed;
86
86
  }
87
+
88
+ export async function callLLMRaw<T = unknown>(
89
+ model: string,
90
+ systemPrompt: string,
91
+ userPrompt: string,
92
+ ): Promise<T> {
93
+ const { apiUrl, apiKey, kind } = getProvider();
94
+
95
+ const headers: Record<string, string> = {
96
+ 'Authorization': `Bearer ${apiKey}`,
97
+ 'Content-Type': 'application/json',
98
+ };
99
+
100
+ if (kind === 'openrouter') {
101
+ headers['HTTP-Referer'] = 'https://opm.dev';
102
+ headers['X-Title'] = 'OPM Security Scanner';
103
+ }
104
+
105
+ const res = await fetch(apiUrl, {
106
+ method: 'POST',
107
+ headers,
108
+ body: JSON.stringify({
109
+ model,
110
+ messages: [
111
+ { role: 'system', content: systemPrompt },
112
+ { role: 'user', content: userPrompt },
113
+ ],
114
+ response_format: { type: 'json_object' },
115
+ temperature: 0.1,
116
+ max_tokens: 4096,
117
+ }),
118
+ });
119
+
120
+ if (!res.ok) {
121
+ const body = await res.text();
122
+ throw new Error(`${kind} ${res.status}: ${body}`);
123
+ }
124
+
125
+ const data = await res.json() as { choices: Array<{ message: { content: string } }> };
126
+ const raw = data.choices?.[0]?.message?.content;
127
+ if (!raw) throw new Error(`Empty response from ${kind}/${model}`);
128
+
129
+ const parsed = safeJsonParse<T>(raw);
130
+ if (!parsed) throw new Error(`Invalid JSON from ${model}: ${raw.slice(0, 200)}`);
131
+ return parsed;
132
+ }
@@ -1,4 +1,4 @@
1
- import type { ScanReport, AgentEntry, Vulnerability, SupplyChainIndicators } from '@opm/core';
1
+ import type { ScanReport, AgentEntry, Vulnerability, SupplyChainIndicators, CheckReport } from '@opm/core';
2
2
 
3
3
  function riskEmoji(score: number): string {
4
4
  if (score >= 70) return '🔴';
@@ -49,10 +49,15 @@ function formatAgent(agent: AgentEntry, index: number): string {
49
49
  const { result } = agent;
50
50
  const emoji = riskEmoji(result.risk_score);
51
51
 
52
+ const modelLines = [`- **Model:** \`${agent.model}\``];
53
+ if (agent.model_intelligence || agent.model_coding) {
54
+ modelLines.push(`- **AI Intelligence Index:** ${agent.model_intelligence || '—'}/100 | **Coding Index:** ${agent.model_coding || '—'}/100 | **Weight:** ${agent.model_weight || '—'}`);
55
+ }
56
+
52
57
  return [
53
58
  `### Agent ${index + 1}: \`${agent.agent_id}\``,
54
59
  '',
55
- `- **Model:** \`${agent.model}\``,
60
+ ...modelLines,
56
61
  `- **Risk Score:** ${emoji} **${result.risk_score}/100** (${result.risk_level})`,
57
62
  `- **Recommendation:** ${result.recommendation}`,
58
63
  '',
@@ -101,10 +106,12 @@ export function formatReportAsMarkdown(report: ScanReport): string {
101
106
  '',
102
107
  '---',
103
108
  '',
104
- '## Aggregate Risk',
109
+ '## Aggregate Risk (Intelligence-Weighted)',
105
110
  '',
106
111
  `\`${riskBar(report.aggregate_risk_score)}\` **${report.aggregate_risk_score}/100** — ${report.consensus}`,
107
112
  '',
113
+ `> Scores are weighted by each model's AI Intelligence and Coding indices from [Artificial Analysis](https://artificialanalysis.ai).`,
114
+ '',
108
115
  report.aggregate_risk_score < 40
109
116
  ? '> ✅ This package appears safe based on multi-agent consensus.'
110
117
  : report.aggregate_risk_score < 70
@@ -132,3 +139,115 @@ export function formatReportAsMarkdown(report: ScanReport): string {
132
139
 
133
140
  return sections.join('\n');
134
141
  }
142
+
143
+ export function formatCheckReportAsMarkdown(report: CheckReport): string {
144
+ const timestamp = new Date(report.timestamp).toLocaleString('en-US', {
145
+ dateStyle: 'long', timeStyle: 'short',
146
+ });
147
+
148
+ const typosquats = report.deps.filter((d) => d.typosquat);
149
+ const cveBlocked = report.deps.filter((d) => d.cveCritical > 0);
150
+ const cveWarned = report.deps.filter((d) => d.cveCount > 0 && d.cveCritical === 0);
151
+ const highRisk = report.deps.filter((d) => d.onChainScore !== null && d.onChainScore >= 70);
152
+ const safeCount = report.totalDeps - typosquats.length - cveBlocked.length - cveWarned.length - highRisk.length;
153
+
154
+ const sections: string[] = [
155
+ `# OPM Dependency Check Report`,
156
+ '',
157
+ `- **Project:** ${report.project}`,
158
+ `- **Scanned:** ${timestamp}`,
159
+ `- **Total dependencies:** ${report.totalDeps}`,
160
+ '',
161
+ '---',
162
+ '',
163
+ '## Summary',
164
+ '',
165
+ `| Category | Count |`,
166
+ `|---|---|`,
167
+ `| ${typosquats.length > 0 ? '🔴' : '🟢'} Typosquats | ${typosquats.length} |`,
168
+ `| ${cveBlocked.length > 0 ? '🔴' : '🟢'} Critical CVEs | ${cveBlocked.length} |`,
169
+ `| ${cveWarned.length > 0 ? '🟡' : '🟢'} CVE Warnings | ${cveWarned.length} |`,
170
+ `| ${highRisk.length > 0 ? '🔴' : '🟢'} High On-chain Risk | ${highRisk.length} |`,
171
+ `| 🟢 Safe | ${Math.max(0, safeCount)} |`,
172
+ '',
173
+ ];
174
+
175
+ if (typosquats.length > 0) {
176
+ sections.push('---', '', '## Typosquat Risks', '');
177
+ for (const d of typosquats) {
178
+ sections.push(
179
+ `### \`${d.name}\`@${d.version}`,
180
+ '',
181
+ `- **Likely intended package:** \`${d.typosquat!.likelyTarget}\``,
182
+ `- **Confidence:** ${d.typosquat!.confidence}`,
183
+ `- **Reason:** ${d.typosquat!.reason}`,
184
+ '',
185
+ );
186
+ }
187
+ }
188
+
189
+ if (cveBlocked.length > 0) {
190
+ sections.push('---', '', '## Critical Vulnerabilities', '');
191
+ for (const d of cveBlocked) {
192
+ sections.push(
193
+ `### \`${d.name}\`@${d.version}`,
194
+ '',
195
+ `- **Critical:** ${d.cveCritical} | **High:** ${d.cveHigh}`,
196
+ `- **CVEs:** ${d.cveIds.join(', ')}`,
197
+ d.fixVersion ? `- **Fix:** upgrade to \`${d.fixVersion}\`` : '',
198
+ '',
199
+ );
200
+ }
201
+ }
202
+
203
+ if (cveWarned.length > 0) {
204
+ sections.push('---', '', '## CVE Warnings', '');
205
+ for (const d of cveWarned) {
206
+ sections.push(
207
+ `- \`${d.name}\`@${d.version} — ${d.cveCount} CVE(s)${d.fixVersion ? ` → \`${d.fixVersion}\`` : ''}`,
208
+ );
209
+ }
210
+ sections.push('');
211
+ }
212
+
213
+ if (highRisk.length > 0) {
214
+ sections.push('---', '', '## High On-chain Risk', '');
215
+ for (const d of highRisk) {
216
+ sections.push(`- \`${d.name}\`@${d.version} — risk score **${d.onChainScore}/100**`);
217
+ }
218
+ sections.push('');
219
+ }
220
+
221
+ if (report.agents.length > 0) {
222
+ sections.push('---', '', '## AI Agent Analysis', '');
223
+ for (const a of report.agents) {
224
+ const flags = a.findings.filter((f) => f.issue !== 'safe' && f.severity !== 'NONE');
225
+ sections.push(
226
+ `### \`${a.agentId}\` — ${a.model}`,
227
+ '',
228
+ `- **AI Intelligence:** ${a.intelligence}/100 | **Coding:** ${a.coding}/100`,
229
+ `- **Risk Score:** ${a.riskScore}/100`,
230
+ '',
231
+ );
232
+ if (flags.length > 0) {
233
+ for (const f of flags) {
234
+ sections.push(
235
+ `- **[${f.severity}]** \`${f.package}\` — ${f.issue}: ${f.explanation}` +
236
+ (f.suggested_replacement ? ` → \`${f.suggested_replacement}\`` : ''),
237
+ );
238
+ }
239
+ } else {
240
+ sections.push(`- ✅ No issues found`);
241
+ }
242
+ sections.push('', `> ${a.overall}`, '');
243
+ }
244
+ }
245
+
246
+ sections.push(
247
+ '---',
248
+ '',
249
+ `*Report generated by [OPM](https://github.com/dhananjaypai08/opm) — On-chain Package Manager*`,
250
+ );
251
+
252
+ return sections.filter((l) => l !== undefined).join('\n');
253
+ }
@@ -0,0 +1,118 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+
3
+ /**
4
+ * Zero-knowledge accuracy verification via hash commitments.
5
+ *
6
+ * The scheme works as follows:
7
+ * 1. A trusted authority generates benchmark test cases with expected outputs
8
+ * 2. Expected outputs are hashed with a secret salt → commitment
9
+ * 3. The candidate agent runs against the benchmarks
10
+ * 4. Actual outputs are hashed with the same salt
11
+ * 5. A proof is generated: hash(commitment || result_hashes || accuracy_flag)
12
+ * 6. The verifier checks the proof without seeing individual test results
13
+ *
14
+ * This ensures:
15
+ * - Test cases remain private (can't be gamed)
16
+ * - Individual results aren't disclosed
17
+ * - Only a binary pass/fail is revealed
18
+ * - The proof is deterministic and verifiable
19
+ */
20
+
21
+ export interface ZKCommitment {
22
+ salt: string;
23
+ expectedHash: string;
24
+ caseCount: number;
25
+ }
26
+
27
+ export interface ZKProof {
28
+ commitment: ZKCommitment;
29
+ resultHash: string;
30
+ accuracyProof: string;
31
+ passed: boolean;
32
+ timestamp: number;
33
+ }
34
+
35
+ export interface AccuracyWitness {
36
+ expectedVerdicts: number[];
37
+ actualVerdicts: number[];
38
+ salt: string;
39
+ }
40
+
41
+ function poseidonHash(...inputs: string[]): string {
42
+ const h = createHash('sha256');
43
+ for (const input of inputs) {
44
+ h.update(input);
45
+ }
46
+ return h.digest('hex');
47
+ }
48
+
49
+ export function generateCommitment(expectedVerdicts: number[]): ZKCommitment {
50
+ const salt = randomBytes(32).toString('hex');
51
+ const verdictStr = expectedVerdicts.join(',');
52
+ const expectedHash = poseidonHash(salt, verdictStr);
53
+
54
+ return {
55
+ salt,
56
+ expectedHash,
57
+ caseCount: expectedVerdicts.length,
58
+ };
59
+ }
60
+
61
+ export function generateProof(
62
+ commitment: ZKCommitment,
63
+ expectedVerdicts: number[],
64
+ actualVerdicts: number[],
65
+ ): ZKProof {
66
+ if (expectedVerdicts.length !== actualVerdicts.length) {
67
+ throw new Error('Verdict arrays must have equal length');
68
+ }
69
+
70
+ const expectedStr = expectedVerdicts.join(',');
71
+ const commitmentCheck = poseidonHash(commitment.salt, expectedStr);
72
+ if (commitmentCheck !== commitment.expectedHash) {
73
+ throw new Error('Commitment verification failed — expected verdicts do not match');
74
+ }
75
+
76
+ const actualStr = actualVerdicts.join(',');
77
+ const resultHash = poseidonHash(commitment.salt, actualStr);
78
+
79
+ const allMatch = expectedVerdicts.every((e, i) => e === actualVerdicts[i]);
80
+
81
+ const accuracyProof = poseidonHash(
82
+ commitment.expectedHash,
83
+ resultHash,
84
+ allMatch ? '1' : '0',
85
+ commitment.salt,
86
+ );
87
+
88
+ return {
89
+ commitment,
90
+ resultHash,
91
+ accuracyProof,
92
+ passed: allMatch,
93
+ timestamp: Date.now(),
94
+ };
95
+ }
96
+
97
+ export function verifyProof(proof: ZKProof): boolean {
98
+ const recomputedProof = poseidonHash(
99
+ proof.commitment.expectedHash,
100
+ proof.resultHash,
101
+ proof.passed ? '1' : '0',
102
+ proof.commitment.salt,
103
+ );
104
+
105
+ return recomputedProof === proof.accuracyProof;
106
+ }
107
+
108
+ export function proofToOnChainBytes(proof: ZKProof): string {
109
+ const payload = JSON.stringify({
110
+ commitment: proof.commitment.expectedHash,
111
+ resultHash: proof.resultHash,
112
+ accuracyProof: proof.accuracyProof,
113
+ passed: proof.passed,
114
+ timestamp: proof.timestamp,
115
+ caseCount: proof.commitment.caseCount,
116
+ });
117
+ return '0x' + Buffer.from(payload).toString('hex');
118
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "pages": {
3
+ "/layout": [
4
+ "static/chunks/webpack.js",
5
+ "static/chunks/main-app.js",
6
+ "static/css/app/layout.css",
7
+ "static/chunks/app/layout.js"
8
+ ],
9
+ "/page": [
10
+ "static/chunks/webpack.js",
11
+ "static/chunks/main-app.js",
12
+ "static/chunks/app/page.js"
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "polyfillFiles": [
3
+ "static/chunks/polyfills.js"
4
+ ],
5
+ "devFiles": [],
6
+ "ampDevFiles": [],
7
+ "lowPriorityFiles": [
8
+ "static/development/_buildManifest.js",
9
+ "static/development/_ssgManifest.js"
10
+ ],
11
+ "rootMainFiles": [
12
+ "static/chunks/webpack.js",
13
+ "static/chunks/main-app.js"
14
+ ],
15
+ "rootMainFilesTree": {},
16
+ "pages": {
17
+ "/_app": []
18
+ },
19
+ "ampFirstPages": []
20
+ }