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.
- package/.env.example +23 -13
- package/.husky/pre-commit +1 -0
- package/README.md +256 -173
- package/bun.lock +4 -4
- package/docs/architecture/agents.mdx +77 -0
- package/docs/architecture/benchmarks.mdx +65 -0
- package/docs/architecture/overview.mdx +58 -0
- package/docs/architecture/scanner.mdx +53 -0
- package/docs/cli/audit.mdx +35 -0
- package/docs/cli/check.mdx +44 -0
- package/docs/cli/fix.mdx +49 -0
- package/docs/cli/info.mdx +44 -0
- package/docs/cli/install.mdx +71 -0
- package/docs/cli/push.mdx +99 -0
- package/docs/cli/register-agent.mdx +80 -0
- package/docs/cli/view.mdx +52 -0
- package/docs/concepts/multi-agent-consensus.mdx +58 -0
- package/docs/concepts/on-chain-registry.mdx +74 -0
- package/docs/concepts/security-model.mdx +76 -0
- package/docs/concepts/zk-agent-verification.mdx +82 -0
- package/docs/configuration.mdx +82 -0
- package/docs/contract/deployment.mdx +57 -0
- package/docs/contract/events.mdx +115 -0
- package/docs/contract/functions.mdx +220 -0
- package/docs/contract/overview.mdx +58 -0
- package/docs/favicon.svg +5 -0
- package/docs/introduction.mdx +43 -0
- package/docs/logo/dark.svg +5 -0
- package/docs/logo/light.svg +5 -0
- package/docs/mint.json +106 -0
- package/docs/quickstart.mdx +133 -0
- package/package.json +7 -6
- package/packages/cli/src/commands/author-view.tsx +9 -1
- package/packages/cli/src/commands/check.tsx +318 -0
- package/packages/cli/src/commands/fix.tsx +294 -0
- package/packages/cli/src/commands/install.tsx +501 -47
- package/packages/cli/src/commands/push.tsx +53 -22
- package/packages/cli/src/commands/register-agent.tsx +227 -0
- package/packages/cli/src/components/AgentScores.tsx +20 -6
- package/packages/cli/src/components/Hyperlink.tsx +30 -0
- package/packages/cli/src/components/ScanReport.tsx +3 -2
- package/packages/cli/src/index.tsx +44 -6
- package/packages/cli/src/services/avatar.ts +43 -6
- package/packages/cli/src/services/chainpatrol.ts +20 -17
- package/packages/cli/src/services/contract.ts +41 -8
- package/packages/cli/src/services/ens.ts +3 -5
- package/packages/cli/src/services/fileverse.ts +12 -13
- package/packages/cli/src/services/typosquat.ts +166 -0
- package/packages/cli/src/services/version.ts +156 -5
- package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
- package/packages/contracts/contracts/OPMRegistry.sol +63 -0
- package/packages/contracts/scripts/deploy.ts +22 -3
- package/packages/core/src/abi.ts +221 -0
- package/packages/core/src/benchmarks.ts +450 -0
- package/packages/core/src/constants.ts +20 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/model-rankings.ts +115 -0
- package/packages/core/src/prompt.ts +58 -0
- package/packages/core/src/types.ts +41 -0
- package/packages/core/src/utils.ts +142 -3
- package/packages/scanner/src/agents/base-agent.ts +13 -3
- package/packages/scanner/src/index.ts +5 -2
- package/packages/scanner/src/queue/memory-queue.ts +8 -3
- package/packages/scanner/src/services/benchmark-runner.ts +114 -0
- package/packages/scanner/src/services/contract-writer.ts +2 -3
- package/packages/scanner/src/services/fileverse.ts +26 -7
- package/packages/scanner/src/services/openrouter.ts +61 -4
- package/packages/scanner/src/services/report-formatter.ts +122 -3
- package/packages/scanner/src/services/zk-verifier.ts +118 -0
- package/packages/web/.next/BUILD_ID +1 -0
- package/packages/web/.next/app-build-manifest.json +26 -0
- package/packages/web/.next/app-path-routes-manifest.json +4 -0
- package/packages/web/.next/build-manifest.json +33 -0
- package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
- package/packages/web/.next/diagnostics/framework.json +1 -0
- package/packages/web/.next/export-marker.json +6 -0
- package/packages/web/.next/images-manifest.json +58 -0
- package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
- package/packages/web/.next/next-server.js.nft.json +1 -0
- package/packages/web/.next/package.json +1 -0
- package/packages/web/.next/prerender-manifest.json +61 -0
- package/packages/web/.next/react-loadable-manifest.json +1 -0
- package/packages/web/.next/required-server-files.json +320 -0
- package/packages/web/.next/routes-manifest.json +53 -0
- package/packages/web/.next/server/app/_not-found/page.js +2 -0
- package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/packages/web/.next/server/app/_not-found.html +1 -0
- package/packages/web/.next/server/app/_not-found.meta +8 -0
- package/packages/web/.next/server/app/_not-found.rsc +16 -0
- package/packages/web/.next/server/app/index.html +1 -0
- package/packages/web/.next/server/app/index.meta +7 -0
- package/packages/web/.next/server/app/index.rsc +20 -0
- package/packages/web/.next/server/app/page.js +2 -0
- package/packages/web/.next/server/app/page.js.nft.json +1 -0
- package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
- package/packages/web/.next/server/app-paths-manifest.json +4 -0
- package/packages/web/.next/server/chunks/611.js +6 -0
- package/packages/web/.next/server/chunks/778.js +30 -0
- package/packages/web/.next/server/functions-config-manifest.json +4 -0
- package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/packages/web/.next/server/middleware-build-manifest.js +1 -0
- package/packages/web/.next/server/middleware-manifest.json +6 -0
- package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/packages/web/.next/server/next-font-manifest.js +1 -0
- package/packages/web/.next/server/next-font-manifest.json +1 -0
- package/packages/web/.next/server/pages/404.html +1 -0
- package/packages/web/.next/server/pages/500.html +1 -0
- package/packages/web/.next/server/pages/_app.js +1 -0
- package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
- package/packages/web/.next/server/pages/_document.js +1 -0
- package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
- package/packages/web/.next/server/pages/_error.js +19 -0
- package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
- package/packages/web/.next/server/pages-manifest.json +6 -0
- package/packages/web/.next/server/server-reference-manifest.js +1 -0
- package/packages/web/.next/server/server-reference-manifest.json +1 -0
- package/packages/web/.next/server/webpack-runtime.js +1 -0
- package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_buildManifest.js +1 -0
- package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_ssgManifest.js +1 -0
- package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
- package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
- package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
- package/packages/web/.next/static/chunks/app/layout-28a489fb4398663f.js +1 -0
- package/packages/web/.next/static/chunks/app/page-e58ccdb78625bce6.js +1 -0
- package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
- package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
- package/packages/web/.next/static/chunks/main-ee293fa6aa18bdd1.js +1 -0
- package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
- package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
- package/packages/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/packages/web/.next/static/chunks/webpack-e1ae44446e7f7355.js +1 -0
- package/packages/web/.next/static/css/21d69157e271f2ab.css +3 -0
- package/packages/web/.next/trace +2 -0
- package/packages/web/.next/types/app/layout.ts +84 -0
- package/packages/web/.next/types/app/page.ts +84 -0
- package/packages/web/.next/types/cache-life.d.ts +141 -0
- package/packages/web/.next/types/package.json +1 -0
- package/packages/web/.next/types/routes.d.ts +57 -0
- package/packages/web/.next/types/validator.ts +61 -0
- package/packages/web/app/globals.css +75 -0
- package/packages/web/app/layout.tsx +26 -0
- package/packages/web/app/page.tsx +361 -0
- package/packages/web/bun.lock +300 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +5 -0
- package/packages/web/package.json +26 -0
- package/packages/web/postcss.config.mjs +8 -0
- package/packages/web/public/favicon.svg +5 -0
- package/packages/web/public/logo.svg +7 -0
- package/packages/web/tailwind.config.ts +48 -0
- package/packages/web/tsconfig.json +21 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { getEnvOrThrow, truncateAddress, classifyRisk } from '@opm/core';
|
|
3
|
+
import { getEnvOrThrow, truncateAddress, classifyRisk, txUrl, contractUrl, addressUrl } from '@opm/core';
|
|
4
4
|
import type { AgentEntry } from '@opm/core';
|
|
5
5
|
import { Header } from '../components/Header';
|
|
6
6
|
import { StatusLine, type Status } from '../components/StatusLine';
|
|
7
7
|
import { RiskBadge } from '../components/RiskBadge';
|
|
8
|
+
import { Hyperlink } from '../components/Hyperlink';
|
|
8
9
|
import { computeChecksum, signChecksumAsync } from '../services/signature';
|
|
9
10
|
import { resolveENSName } from '../services/ens';
|
|
10
11
|
import { registerPackageOnChain } from '../services/contract';
|
|
@@ -71,7 +72,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
71
72
|
if (!name || !version) throw new Error('package.json missing name or version');
|
|
72
73
|
setPkgLabel(`${name}@${version}`);
|
|
73
74
|
|
|
74
|
-
const privateKey = getEnvOrThrow('OPM_PRIVATE_KEY');
|
|
75
|
+
const privateKey = getEnvOrThrow('OPM_SIGNING_KEY', 'OPM_PRIVATE_KEY');
|
|
75
76
|
|
|
76
77
|
updateStep('pack', 'running');
|
|
77
78
|
const tarball = execSync('npm pack --json 2>/dev/null', { encoding: 'utf-8' });
|
|
@@ -242,26 +243,44 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
242
243
|
<Box flexDirection="column" marginTop={1}>
|
|
243
244
|
<Text color="gray">────────────────────────────────────────</Text>
|
|
244
245
|
<Text color="white" bold> Agent Results</Text>
|
|
245
|
-
{result.agents.map((agent) =>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
{result.agents.map((agent) => {
|
|
247
|
+
const intel = agent.model_intelligence || 0;
|
|
248
|
+
const coding = agent.model_coding || 0;
|
|
249
|
+
const weight = agent.model_weight || 0;
|
|
250
|
+
return (
|
|
251
|
+
<Box key={agent.agent_id} flexDirection="column" marginLeft={2} marginTop={1}>
|
|
252
|
+
<Box>
|
|
253
|
+
<Text color="white" bold>{agent.agent_id}</Text>
|
|
254
|
+
<Text color="gray"> ({agent.model}) </Text>
|
|
255
|
+
<Text color={riskColor(agent.result.risk_score)} bold>
|
|
256
|
+
{agent.result.risk_score}/100
|
|
257
|
+
</Text>
|
|
258
|
+
<Text color="gray"> {agent.result.risk_level}</Text>
|
|
259
|
+
</Box>
|
|
259
260
|
<Box marginLeft={2}>
|
|
260
|
-
<Text color="
|
|
261
|
+
<Text color="magenta">AI Index: {intel}</Text>
|
|
262
|
+
<Text color="gray"> | </Text>
|
|
263
|
+
<Text color="blue">Coding: {coding}</Text>
|
|
264
|
+
<Text color="gray"> | </Text>
|
|
265
|
+
<Text color="cyan">Weight: {weight}</Text>
|
|
261
266
|
</Box>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
<Box marginLeft={2}>
|
|
268
|
+
<Text color="gray" wrap="wrap">{agent.result.reasoning.slice(0, 200)}</Text>
|
|
269
|
+
</Box>
|
|
270
|
+
{agent.result.vulnerabilities.length > 0 && (
|
|
271
|
+
<Box marginLeft={2}>
|
|
272
|
+
<Text color="yellow">{agent.result.vulnerabilities.length} vulnerabilities found</Text>
|
|
273
|
+
</Box>
|
|
274
|
+
)}
|
|
275
|
+
{agent.score_tx_hash && (
|
|
276
|
+
<Box marginLeft={2}>
|
|
277
|
+
<Text color="gray">⛓ </Text>
|
|
278
|
+
<Hyperlink url={txUrl(agent.score_tx_hash)} label={`score tx ${agent.score_tx_hash.slice(0, 10)}…`} color="cyan" />
|
|
279
|
+
</Box>
|
|
280
|
+
)}
|
|
281
|
+
</Box>
|
|
282
|
+
);
|
|
283
|
+
})}
|
|
265
284
|
</Box>
|
|
266
285
|
)}
|
|
267
286
|
|
|
@@ -269,7 +288,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
269
288
|
<Box flexDirection="column" marginTop={1}>
|
|
270
289
|
<Text color="gray">────────────────────────────────────────</Text>
|
|
271
290
|
<Box>
|
|
272
|
-
<Text color="white" bold> Aggregate Risk: </Text>
|
|
291
|
+
<Text color="white" bold> Aggregate Risk (intelligence-weighted): </Text>
|
|
273
292
|
<RiskBadge level={classifyRisk(result.riskScore)} score={result.riskScore} />
|
|
274
293
|
</Box>
|
|
275
294
|
</Box>
|
|
@@ -294,6 +313,18 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
294
313
|
</Box>
|
|
295
314
|
)}
|
|
296
315
|
<StatusLine label="Register on-chain" status={steps.register} detail={result.txHash?.slice(0, 16)} />
|
|
316
|
+
{result.txHash && (
|
|
317
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
318
|
+
<Box>
|
|
319
|
+
<Text color="gray">⛓ </Text>
|
|
320
|
+
<Hyperlink url={txUrl(result.txHash)} label={`tx ${result.txHash.slice(0, 10)}…`} color="green" />
|
|
321
|
+
</Box>
|
|
322
|
+
<Box>
|
|
323
|
+
<Text color="gray">📋 </Text>
|
|
324
|
+
<Hyperlink url={contractUrl()} label="OPM Registry Contract" color="cyan" />
|
|
325
|
+
</Box>
|
|
326
|
+
</Box>
|
|
327
|
+
)}
|
|
297
328
|
</>
|
|
298
329
|
)}
|
|
299
330
|
|
|
@@ -309,7 +340,7 @@ export function PushCommand({ npmToken, otp }: PushCommandProps) {
|
|
|
309
340
|
</Box>
|
|
310
341
|
{!result.reportURI.startsWith('local://') && (
|
|
311
342
|
<Box marginLeft={2}>
|
|
312
|
-
<
|
|
343
|
+
<Hyperlink url={result.reportURI} />
|
|
313
344
|
</Box>
|
|
314
345
|
)}
|
|
315
346
|
</Box>
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { txUrl, contractUrl, addressUrl } from '@opm/core';
|
|
4
|
+
import { Header } from '../components/Header';
|
|
5
|
+
import { StatusLine, type Status } from '../components/StatusLine';
|
|
6
|
+
import { Hyperlink } from '../components/Hyperlink';
|
|
7
|
+
import { registerAgentOnChain } from '../services/contract';
|
|
8
|
+
import { runBenchmarkSuite, type BenchmarkRunResult } from '@opm/scanner';
|
|
9
|
+
|
|
10
|
+
type StepStatus = Status;
|
|
11
|
+
|
|
12
|
+
interface Steps {
|
|
13
|
+
validate: StepStatus;
|
|
14
|
+
benchmark: StepStatus;
|
|
15
|
+
zkproof: StepStatus;
|
|
16
|
+
register: StepStatus;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RegisterResult {
|
|
20
|
+
agentName?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
benchmarkResult?: BenchmarkRunResult;
|
|
23
|
+
txHash?: string;
|
|
24
|
+
agentAddress?: string;
|
|
25
|
+
rejected?: boolean;
|
|
26
|
+
rejectReason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface RegisterAgentCommandProps {
|
|
30
|
+
agentName: string;
|
|
31
|
+
model: string;
|
|
32
|
+
systemPrompt?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function RegisterAgentCommand({ agentName, model, systemPrompt }: RegisterAgentCommandProps) {
|
|
36
|
+
const [steps, setSteps] = useState<Steps>({
|
|
37
|
+
validate: 'pending',
|
|
38
|
+
benchmark: 'pending',
|
|
39
|
+
zkproof: 'pending',
|
|
40
|
+
register: 'pending',
|
|
41
|
+
});
|
|
42
|
+
const [result, setResult] = useState<RegisterResult>({});
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
const [logs, setLogs] = useState<string[]>([]);
|
|
45
|
+
|
|
46
|
+
const updateStep = (key: keyof Steps, status: StepStatus) =>
|
|
47
|
+
setSteps((prev) => ({ ...prev, [key]: status }));
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
runRegistration().catch((err) => setError(String(err)));
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
async function runRegistration() {
|
|
54
|
+
setResult({ agentName, model });
|
|
55
|
+
|
|
56
|
+
updateStep('validate', 'running');
|
|
57
|
+
|
|
58
|
+
if (!agentName || agentName.length < 2) {
|
|
59
|
+
throw new Error('Agent name must be at least 2 characters');
|
|
60
|
+
}
|
|
61
|
+
if (!model) {
|
|
62
|
+
throw new Error('Model identifier is required (e.g. anthropic/claude-sonnet-4-20250514)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!process.env.AGENT_PRIVATE_KEY) {
|
|
66
|
+
throw new Error('AGENT_PRIVATE_KEY required — this wallet becomes the agent identity');
|
|
67
|
+
}
|
|
68
|
+
if (!process.env.OPENROUTER_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
69
|
+
throw new Error('OPENROUTER_API_KEY or OPENAI_API_KEY required to run benchmarks');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
updateStep('validate', 'done');
|
|
73
|
+
|
|
74
|
+
updateStep('benchmark', 'running');
|
|
75
|
+
const benchResult = await runBenchmarkSuite(
|
|
76
|
+
{ name: agentName, model, systemPrompt },
|
|
77
|
+
(msg) => setLogs((prev) => [...prev.slice(-12), msg]),
|
|
78
|
+
);
|
|
79
|
+
setResult((r) => ({ ...r, benchmarkResult: benchResult }));
|
|
80
|
+
|
|
81
|
+
if (!benchResult.zkProof.passed || benchResult.accuracyPct < 100) {
|
|
82
|
+
updateStep('benchmark', 'error');
|
|
83
|
+
updateStep('zkproof', 'error');
|
|
84
|
+
updateStep('register', 'blocked');
|
|
85
|
+
setResult((r) => ({
|
|
86
|
+
...r,
|
|
87
|
+
rejected: true,
|
|
88
|
+
rejectReason: `Agent achieved ${benchResult.accuracyPct}% accuracy (100% required). ` +
|
|
89
|
+
`Failed ${benchResult.failed}/${benchResult.total} benchmark cases.`,
|
|
90
|
+
}));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
updateStep('benchmark', 'done');
|
|
94
|
+
|
|
95
|
+
updateStep('zkproof', 'running');
|
|
96
|
+
if (!benchResult.verified) {
|
|
97
|
+
updateStep('zkproof', 'error');
|
|
98
|
+
updateStep('register', 'blocked');
|
|
99
|
+
setResult((r) => ({
|
|
100
|
+
...r,
|
|
101
|
+
rejected: true,
|
|
102
|
+
rejectReason: 'ZK proof verification failed — integrity check did not pass',
|
|
103
|
+
}));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setLogs((prev) => [...prev, `ZK proof hash: ${benchResult.zkProof.accuracyProof.slice(0, 24)}…`]);
|
|
107
|
+
updateStep('zkproof', 'done');
|
|
108
|
+
|
|
109
|
+
updateStep('register', 'running');
|
|
110
|
+
try {
|
|
111
|
+
const proofStr = benchResult.zkProof.accuracyProof;
|
|
112
|
+
const promptStr = systemPrompt || 'default-opm-security-prompt';
|
|
113
|
+
const txHash = await registerAgentOnChain(agentName, model, promptStr, proofStr);
|
|
114
|
+
setResult((r) => ({ ...r, txHash }));
|
|
115
|
+
setLogs((prev) => [...prev, `Agent registered on-chain ✓`]);
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
const msg = err?.shortMessage || err?.message || 'failed';
|
|
118
|
+
setLogs((prev) => [...prev, `Registration: ${msg}`]);
|
|
119
|
+
if (msg.includes('already')) {
|
|
120
|
+
setResult((r) => ({ ...r, rejected: true, rejectReason: 'Agent wallet is already registered' }));
|
|
121
|
+
updateStep('register', 'error');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
updateStep('register', 'done');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const riskColor = (pct: number) => (pct >= 100 ? 'green' : pct >= 70 ? 'yellow' : 'red');
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Box flexDirection="column">
|
|
132
|
+
<Header subtitle="register-agent" />
|
|
133
|
+
<Text color="white" bold> Registering agent: {agentName}</Text>
|
|
134
|
+
<Text color="gray"> Model: {model}</Text>
|
|
135
|
+
<Text> </Text>
|
|
136
|
+
|
|
137
|
+
<StatusLine label="Validate configuration" status={steps.validate} />
|
|
138
|
+
<StatusLine label="Run benchmark suite (10 cases)" status={steps.benchmark} />
|
|
139
|
+
|
|
140
|
+
{logs.length > 0 && (
|
|
141
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
142
|
+
{logs.map((log, i) => (
|
|
143
|
+
<Text key={i} color="gray">{log}</Text>
|
|
144
|
+
))}
|
|
145
|
+
</Box>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{result.benchmarkResult && (
|
|
149
|
+
<Box flexDirection="column" marginTop={1}>
|
|
150
|
+
<Text color="gray">────────────────────────────────────────</Text>
|
|
151
|
+
<Text color="white" bold> Benchmark Results</Text>
|
|
152
|
+
<Box flexDirection="column" marginLeft={2} marginTop={1}>
|
|
153
|
+
{result.benchmarkResult.results.map((r) => (
|
|
154
|
+
<Box key={r.caseId}>
|
|
155
|
+
<Text color={r.verdict === 'PASS' ? 'green' : 'red'}>
|
|
156
|
+
{r.verdict === 'PASS' ? '✓' : '✗'}{' '}
|
|
157
|
+
</Text>
|
|
158
|
+
<Text color="white">{r.category}</Text>
|
|
159
|
+
<Text color="gray"> — expected {r.expectedLevel}, got {r.actualLevel}</Text>
|
|
160
|
+
<Text color="gray"> (score: {r.actualScore})</Text>
|
|
161
|
+
</Box>
|
|
162
|
+
))}
|
|
163
|
+
</Box>
|
|
164
|
+
<Box marginLeft={2} marginTop={1}>
|
|
165
|
+
<Text color={riskColor(result.benchmarkResult.accuracyPct)} bold>
|
|
166
|
+
Accuracy: {result.benchmarkResult.passed}/{result.benchmarkResult.total}{' '}
|
|
167
|
+
({result.benchmarkResult.accuracyPct}%)
|
|
168
|
+
</Text>
|
|
169
|
+
</Box>
|
|
170
|
+
</Box>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
<Text> </Text>
|
|
174
|
+
<StatusLine label="ZK proof verification" status={steps.zkproof} />
|
|
175
|
+
{result.benchmarkResult?.zkProof && steps.zkproof === 'done' && (
|
|
176
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
177
|
+
<Text color="gray">Commitment: {result.benchmarkResult.zkProof.commitment.expectedHash.slice(0, 24)}…</Text>
|
|
178
|
+
<Text color="gray">Proof: {result.benchmarkResult.zkProof.accuracyProof.slice(0, 24)}…</Text>
|
|
179
|
+
<Text color="green">✓ Zero-knowledge proof verified — accuracy proven without revealing test data</Text>
|
|
180
|
+
</Box>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<StatusLine label="Register agent on-chain" status={steps.register} />
|
|
184
|
+
{result.txHash && (
|
|
185
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
186
|
+
<Box>
|
|
187
|
+
<Text color="gray">⛓ </Text>
|
|
188
|
+
<Hyperlink url={txUrl(result.txHash)} label={`tx ${result.txHash.slice(0, 10)}…`} color="green" />
|
|
189
|
+
</Box>
|
|
190
|
+
<Box>
|
|
191
|
+
<Text color="gray">📋 </Text>
|
|
192
|
+
<Hyperlink url={contractUrl()} label="OPM Registry Contract" color="cyan" />
|
|
193
|
+
</Box>
|
|
194
|
+
</Box>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{result.rejected && (
|
|
198
|
+
<Box flexDirection="column" marginTop={1}>
|
|
199
|
+
<Text color="gray">────────────────────────────────────────</Text>
|
|
200
|
+
<Text color="red" bold>✗ REGISTRATION REJECTED</Text>
|
|
201
|
+
<Box marginLeft={2}>
|
|
202
|
+
<Text color="red" wrap="wrap">{result.rejectReason}</Text>
|
|
203
|
+
</Box>
|
|
204
|
+
{result.benchmarkResult && result.benchmarkResult.failureReasons.length > 0 && (
|
|
205
|
+
<Box flexDirection="column" marginLeft={2} marginTop={1}>
|
|
206
|
+
<Text color="yellow" bold>Failure details:</Text>
|
|
207
|
+
{result.benchmarkResult.failureReasons.map((reason, i) => (
|
|
208
|
+
<Text key={i} color="yellow" wrap="wrap"> • {reason}</Text>
|
|
209
|
+
))}
|
|
210
|
+
</Box>
|
|
211
|
+
)}
|
|
212
|
+
</Box>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{!result.rejected && steps.register === 'done' && (
|
|
216
|
+
<Box flexDirection="column" marginTop={1}>
|
|
217
|
+
<Text color="gray">────────────────────────────────────────</Text>
|
|
218
|
+
<Text color="green" bold>✓ Agent "{agentName}" registered successfully</Text>
|
|
219
|
+
<Text color="gray"> Your agent is now authorized to submit security scores on-chain.</Text>
|
|
220
|
+
<Text color="gray"> It will participate in the next package scan alongside existing agents.</Text>
|
|
221
|
+
</Box>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{error && <Text color="red">Error: {error}</Text>}
|
|
225
|
+
</Box>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -17,13 +17,27 @@ export function AgentScores({ agents }: AgentScoresProps) {
|
|
|
17
17
|
const level = classifyRisk(agent.result.risk_score);
|
|
18
18
|
const color = RISK_COLORS[level];
|
|
19
19
|
const connector = i === agents.length - 1 ? '└──' : '├──';
|
|
20
|
+
const intel = agent.model_intelligence || 0;
|
|
21
|
+
const coding = agent.model_coding || 0;
|
|
22
|
+
const weight = agent.model_weight || 0;
|
|
20
23
|
return (
|
|
21
|
-
<Box key={agent.agent_id}>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
<Box key={agent.agent_id} flexDirection="column">
|
|
25
|
+
<Box>
|
|
26
|
+
<Text color="gray">{connector} </Text>
|
|
27
|
+
<Text color="cyan">{agent.agent_id}</Text>
|
|
28
|
+
<Text color="gray"> ({agent.model}) </Text>
|
|
29
|
+
<Text color={color} bold>{agent.result.risk_score}/100</Text>
|
|
30
|
+
<Text color="gray"> {agent.result.recommendation}</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
{(intel > 0 || coding > 0) && (
|
|
33
|
+
<Box marginLeft={4}>
|
|
34
|
+
<Text color="magenta">AI: {intel}</Text>
|
|
35
|
+
<Text color="gray"> | </Text>
|
|
36
|
+
<Text color="blue">Code: {coding}</Text>
|
|
37
|
+
<Text color="gray"> | </Text>
|
|
38
|
+
<Text color="cyan">W: {weight}</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
)}
|
|
27
41
|
</Box>
|
|
28
42
|
);
|
|
29
43
|
})}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface HyperlinkProps {
|
|
5
|
+
url: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Hyperlink({ url, label, color = 'cyan' }: HyperlinkProps) {
|
|
11
|
+
const display = label || shortenUrl(url);
|
|
12
|
+
const ansi = `\x1b]8;;${url}\x07${display}\x1b]8;;\x07`;
|
|
13
|
+
return <Text color={color as any}>{ansi}</Text>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shortenUrl(url: string): string {
|
|
17
|
+
try {
|
|
18
|
+
const u = new URL(url);
|
|
19
|
+
const pathParts = u.pathname.split('/').filter(Boolean);
|
|
20
|
+
const hash = u.hash;
|
|
21
|
+
if (pathParts.length >= 2) {
|
|
22
|
+
const id = pathParts[pathParts.length - 1];
|
|
23
|
+
const shortHash = hash.length > 20 ? hash.slice(0, 20) + '...' : hash;
|
|
24
|
+
return `${u.host}/.../${id}${shortHash}`;
|
|
25
|
+
}
|
|
26
|
+
return url.length > 60 ? url.slice(0, 57) + '...' : url;
|
|
27
|
+
} catch {
|
|
28
|
+
return url;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import type { ScanReport as ScanReportType } from '@opm/core';
|
|
4
|
+
import { Hyperlink } from './Hyperlink';
|
|
4
5
|
|
|
5
6
|
interface ScanReportProps {
|
|
6
7
|
report?: ScanReportType | null;
|
|
@@ -21,10 +22,10 @@ export function ScanReport({ report, reportURI }: ScanReportProps) {
|
|
|
21
22
|
return (
|
|
22
23
|
<Box flexDirection="column" marginLeft={2}>
|
|
23
24
|
<Text bold color="white"> Scan Report</Text>
|
|
24
|
-
{reportURI && (
|
|
25
|
+
{reportURI && !reportURI.startsWith('local://') && (
|
|
25
26
|
<Box>
|
|
26
27
|
<Text color="gray"> Link: </Text>
|
|
27
|
-
<
|
|
28
|
+
<Hyperlink url={reportURI} />
|
|
28
29
|
</Box>
|
|
29
30
|
)}
|
|
30
31
|
{report && (
|
|
@@ -6,7 +6,10 @@ import { InstallCommand } from './commands/install';
|
|
|
6
6
|
import { AuditCommand } from './commands/audit';
|
|
7
7
|
import { InfoCommand } from './commands/info';
|
|
8
8
|
import { AuthorViewCommand } from './commands/author-view';
|
|
9
|
+
import { CheckCommand } from './commands/check';
|
|
10
|
+
import { FixCommand } from './commands/fix';
|
|
9
11
|
import { PassthroughCommand } from './commands/passthrough';
|
|
12
|
+
import { RegisterAgentCommand } from './commands/register-agent';
|
|
10
13
|
import { Header } from './components/Header';
|
|
11
14
|
|
|
12
15
|
const args = process.argv.slice(2);
|
|
@@ -56,11 +59,38 @@ function App() {
|
|
|
56
59
|
if (!name) return <Help />;
|
|
57
60
|
return <InfoCommand packageName={name} version={version} />;
|
|
58
61
|
}
|
|
62
|
+
case 'check':
|
|
63
|
+
return <CheckCommand />;
|
|
64
|
+
case 'fix':
|
|
65
|
+
return <FixCommand />;
|
|
59
66
|
case 'whois': {
|
|
60
67
|
if (!rest[0]) return <Help />;
|
|
61
68
|
const ensArg = rest[0].endsWith('.eth') ? rest[0] : `${rest[0]}.eth`;
|
|
62
69
|
return <AuthorViewCommand ensName={ensArg} />;
|
|
63
70
|
}
|
|
71
|
+
case 'register-agent': {
|
|
72
|
+
const nameIdx = rest.findIndex((a) => a === '--name');
|
|
73
|
+
const modelIdx = rest.findIndex((a) => a === '--model');
|
|
74
|
+
const promptIdx = rest.findIndex((a) => a === '--system-prompt');
|
|
75
|
+
const agentName = nameIdx >= 0 ? rest[nameIdx + 1] : undefined;
|
|
76
|
+
const agentModel = modelIdx >= 0 ? rest[modelIdx + 1] : undefined;
|
|
77
|
+
const systemPrompt = promptIdx >= 0 ? rest[promptIdx + 1] : undefined;
|
|
78
|
+
if (!agentName || !agentModel) {
|
|
79
|
+
return (
|
|
80
|
+
<Box flexDirection="column">
|
|
81
|
+
<Header />
|
|
82
|
+
<Text color="red">Usage: opm register-agent --name {'<name>'} --model {'<model>'} [--system-prompt {'<prompt>'}]</Text>
|
|
83
|
+
<Text color="gray"> --name Agent identifier (e.g. my-security-agent)</Text>
|
|
84
|
+
<Text color="gray"> --model LLM model to use (e.g. anthropic/claude-sonnet-4-20250514)</Text>
|
|
85
|
+
<Text color="gray"> --system-prompt Custom system prompt (defaults to OPM security auditor prompt)</Text>
|
|
86
|
+
<Text> </Text>
|
|
87
|
+
<Text color="gray">The agent will be benchmarked against 10 labeled security test cases.</Text>
|
|
88
|
+
<Text color="gray">A ZK proof of 100% accuracy is required for registration.</Text>
|
|
89
|
+
</Box>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return <RegisterAgentCommand agentName={agentName} model={agentModel} systemPrompt={systemPrompt} />;
|
|
93
|
+
}
|
|
64
94
|
default:
|
|
65
95
|
if (command && PASSTHROUGH.has(command)) {
|
|
66
96
|
return <PassthroughCommand command={command} args={rest} />;
|
|
@@ -76,12 +106,21 @@ function Help() {
|
|
|
76
106
|
<Box flexDirection="column" marginLeft={2}>
|
|
77
107
|
<Text color="cyan" bold>Security commands:</Text>
|
|
78
108
|
<Text> opm push [--token t] [--otp c] Sign, scan, publish, register</Text>
|
|
79
|
-
<Text> opm install [pkg]
|
|
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>
|
|
111
|
+
<Text> opm check Scan all deps: typosquats, CVEs, AI analysis</Text>
|
|
112
|
+
<Text> opm fix Auto-fix typosquats and vulnerable versions</Text>
|
|
80
113
|
<Text> opm audit Scan all deps against on-chain security data</Text>
|
|
81
114
|
<Text> opm info {'<pkg>'} Show on-chain security info for a package</Text>
|
|
82
115
|
<Text> opm view {'<name.eth>'} Show author profile, packages, and risk scores</Text>
|
|
83
116
|
<Text> opm whois {'<name>'} Look up an ENS identity on OPM</Text>
|
|
84
117
|
<Text> </Text>
|
|
118
|
+
<Text color="cyan" bold>Agent commands:</Text>
|
|
119
|
+
<Text> opm register-agent Register a new security agent (ZK-verified)</Text>
|
|
120
|
+
<Text> --name {'<name>'} Agent identifier</Text>
|
|
121
|
+
<Text> --model {'<model>'} LLM model (e.g. anthropic/claude-sonnet-4-20250514)</Text>
|
|
122
|
+
<Text> --system-prompt {'<p>'} Custom system prompt (optional)</Text>
|
|
123
|
+
<Text> </Text>
|
|
85
124
|
<Text color="cyan" bold>Standard commands (npm passthrough):</Text>
|
|
86
125
|
<Text> opm init Initialize a new package</Text>
|
|
87
126
|
<Text> opm run {'<script>'} Run a package script</Text>
|
|
@@ -97,12 +136,11 @@ function Help() {
|
|
|
97
136
|
<Text> </Text>
|
|
98
137
|
<Text color="gray">Aliases: i/add → install, rm → uninstall, ls → list</Text>
|
|
99
138
|
<Text color="gray"> view name.eth → author profile, view pkg → info</Text>
|
|
139
|
+
<Text color="gray"> pkg@name.eth → ENS-resolved safest version by author</Text>
|
|
100
140
|
<Text> </Text>
|
|
101
|
-
<Text color="cyan" bold>Environment:</Text>
|
|
102
|
-
<Text>
|
|
103
|
-
<Text>
|
|
104
|
-
<Text> OPENAI_API_KEY For AI security scanning</Text>
|
|
105
|
-
<Text> NPM_TOKEN npm automation token (alt to --token)</Text>
|
|
141
|
+
<Text color="cyan" bold>Environment (install/audit/info/view need no config):</Text>
|
|
142
|
+
<Text> OPM_SIGNING_KEY Author signing key (for push only)</Text>
|
|
143
|
+
<Text> NPM_TOKEN npm automation token (for push only)</Text>
|
|
106
144
|
</Box>
|
|
107
145
|
</Box>
|
|
108
146
|
);
|
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Jimp, intToRGBA } from 'jimp';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const PIXEL = '\u2584';
|
|
4
|
+
|
|
5
|
+
export async function renderAvatar(url: string, width = 24): Promise<string | null> {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
4
8
|
try {
|
|
5
|
-
const res = await fetch(url, { redirect: 'follow' });
|
|
9
|
+
const res = await fetch(url, { redirect: 'follow', signal: controller.signal });
|
|
10
|
+
clearTimeout(timeout);
|
|
6
11
|
if (!res.ok) return null;
|
|
7
12
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
8
|
-
return
|
|
9
|
-
} catch {
|
|
10
|
-
|
|
13
|
+
return renderImageToAnsi(buffer, width);
|
|
14
|
+
} catch {
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function renderImageToAnsi(buffer: Buffer, targetWidth: number): Promise<string | null> {
|
|
21
|
+
const image = await Jimp.fromBuffer(buffer);
|
|
22
|
+
const { width: origW, height: origH } = image;
|
|
23
|
+
|
|
24
|
+
const ratio = origH / origW;
|
|
25
|
+
const w = targetWidth;
|
|
26
|
+
const h = Math.max(2, Math.round(w * ratio));
|
|
27
|
+
|
|
28
|
+
image.resize({ w, h });
|
|
29
|
+
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
for (let y = 0; y < h - 1; y += 2) {
|
|
32
|
+
let line = '';
|
|
33
|
+
for (let x = 0; x < w; x++) {
|
|
34
|
+
const top = intToRGBA(image.getPixelColor(x, y));
|
|
35
|
+
const bot = intToRGBA(image.getPixelColor(x, y + 1));
|
|
36
|
+
|
|
37
|
+
if (top.a === 0) {
|
|
38
|
+
line += `\x1b[0m\x1b[38;2;${bot.r};${bot.g};${bot.b}m${PIXEL}\x1b[0m`;
|
|
39
|
+
} else {
|
|
40
|
+
line += `\x1b[48;2;${top.r};${top.g};${top.b}m\x1b[38;2;${bot.r};${bot.g};${bot.b}m${PIXEL}\x1b[0m`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
lines.push(line);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
import { CHAINPATROL_API_URL
|
|
1
|
+
import { CHAINPATROL_API_URL } from '@opm/core';
|
|
2
2
|
import type { ChainPatrolResult } from '@opm/core';
|
|
3
3
|
|
|
4
4
|
export async function checkPackageWithChainPatrol(packageName: string): Promise<ChainPatrolResult> {
|
|
5
|
-
const apiKey =
|
|
5
|
+
const apiKey = process.env.CHAINPATROL_API_KEY;
|
|
6
|
+
if (!apiKey) return { status: 'UNKNOWN', source: 'skipped' };
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(`${CHAINPATROL_API_URL}/asset/check`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'X-API-KEY': apiKey,
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({ content: `npm:${packageName}` }),
|
|
16
|
+
});
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
if (!res.ok) return { status: 'UNKNOWN', source: 'error' };
|
|
19
|
+
|
|
20
|
+
const data = await res.json() as { status: string; source: string };
|
|
21
|
+
return {
|
|
22
|
+
status: (data.status as ChainPatrolResult['status']) || 'UNKNOWN',
|
|
23
|
+
source: data.source || 'chainpatrol',
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
17
26
|
return { status: 'UNKNOWN', source: 'error' };
|
|
18
27
|
}
|
|
19
|
-
|
|
20
|
-
const data = await res.json() as { status: string; source: string };
|
|
21
|
-
return {
|
|
22
|
-
status: (data.status as ChainPatrolResult['status']) || 'UNKNOWN',
|
|
23
|
-
source: data.source || 'chainpatrol',
|
|
24
|
-
};
|
|
25
28
|
}
|