solguard-cli 0.1.0

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/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # solguard-cli
2
+
3
+ Run SolGuard security agents from your terminal.
4
+
5
+ ## Install / run
6
+
7
+ ```bash
8
+ npx solguard-cli
9
+ ```
10
+
11
+ No global install required. Works with `npm install -g solguard-cli` as well.
12
+
13
+ Requires **Node.js 18+**.
14
+
15
+ ## Modes
16
+
17
+ | Mode | Description |
18
+ |------|-------------|
19
+ | **Free** | Uses SolGuard API + your 2 signup credits (same as website). Identity: anonymous `cliInstallId` in `~/.solguard/config.json`. |
20
+ | **Premium (Beta)** | Bring your own OpenAI, Anthropic, or Gemini key — runs **locally** on your machine. Key never sent to SolGuard. Six services supported. |
21
+
22
+ ## Config
23
+
24
+ `~/.solguard/config.json` (mode `600` on Unix):
25
+
26
+ ```json
27
+ {
28
+ "cliInstallId": "<uuid>",
29
+ "token": "<jwt>",
30
+ "baseUrl": "https://www.solguard.space/api",
31
+ "savedKeys": {}
32
+ }
33
+ ```
34
+
35
+ ## Premium local (Beta)
36
+
37
+ - **BYOK LLM required:** Cyber Security Consultant, Solana Token Verification
38
+ - **Local only (no API key):** Wallet Verification, dApp Frontend Scan, OpenClaw Audit, Smart Contract Audit
39
+ - **On-chain agents:** set `HELIUS_API_KEY` in your environment
40
+
41
+ ## Local development
42
+
43
+ 1. Start the API: `npm run dev` (from repo root)
44
+ 2. Run the CLI — it auto-detects `http://localhost:3000/api` when the dev server is up
45
+
46
+ Or set explicitly:
47
+
48
+ ```powershell
49
+ $env:SOLGUARD_API = "http://localhost:3000/api"
50
+ node bin/solguard-cli.js
51
+ ```
52
+
53
+ ## Publish (maintainers)
54
+
55
+ ```bash
56
+ npm login
57
+ cd packages/solguard-cli
58
+ npm publish --access public
59
+ ```
60
+
61
+ **Do not publish without explicit approval.**
62
+
63
+ ## QA
64
+
65
+ ```bash
66
+ node scripts/qa-cli-auth.mjs http://localhost:3000/api
67
+ node scripts/qa-cli-local-premium.mjs
68
+ ```
69
+
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { runWizard } from "../src/wizard.js";
3
+
4
+ /** Strip common API key patterns from error output. */
5
+ function redactSecrets(text) {
6
+ return String(text || "")
7
+ .replace(/sk-[a-zA-Z0-9_-]{10,}/g, "[REDACTED]")
8
+ .replace(/sk-ant-[a-zA-Z0-9_-]+/g, "[REDACTED]")
9
+ .replace(/AIza[a-zA-Z0-9_-]{20,}/g, "[REDACTED]")
10
+ .replace(/x-api-key['":\s]+[a-zA-Z0-9_-]+/gi, "x-api-key: [REDACTED]")
11
+ .replace(/Bearer\s+[a-zA-Z0-9._-]+/gi, "Bearer [REDACTED]");
12
+ }
13
+
14
+ runWizard().catch((err) => {
15
+ const msg = redactSecrets(err?.message || String(err));
16
+ const cause = err?.cause?.message ? redactSecrets(err.cause.message) : null;
17
+ console.error(`\nError: ${msg}${cause ? ` (${cause})` : ""}\n`);
18
+ process.exit(1);
19
+ });
@@ -0,0 +1,57 @@
1
+ import { buildReport, finding } from "../reportBuilder.js";
2
+ import { buildConsultantMessages, parseConsultantResponse } from "../llm/prompts/consultant.js";
3
+
4
+ const DEFAULT_DATA_SOURCE = ["OpenRouter AI"];
5
+
6
+ function logConsultantError(e, extra = {}, model = "unknown") {
7
+ console.error("[LLM] AI consultant failed:", {
8
+ status: e?.status ?? e?.code ?? "unknown",
9
+ message: e?.message,
10
+ model,
11
+ ...extra,
12
+ });
13
+ }
14
+
15
+ /**
16
+ * Run the AI security consultant agent — shared by Next.js backend and CLI BYOK.
17
+ * @param {{ query: string }} inputs
18
+ * @param {{ llmClient: { complete: Function, model: string }, dataSource?: string[], maxAttempts?: number }} opts
19
+ */
20
+ export async function runConsultant({ query }, { llmClient, dataSource = DEFAULT_DATA_SOURCE, maxAttempts = 2 } = {}) {
21
+ if (!query?.trim()) throw new Error("query is required");
22
+ if (!llmClient?.complete) throw new Error("llmClient is required");
23
+
24
+ const messages = buildConsultantMessages(query.trim());
25
+ let lastError = null;
26
+
27
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
28
+ try {
29
+ const text = await llmClient.complete(messages, { maxTokens: 600, temperature: 0.6 });
30
+ if (text) {
31
+ const parsed = parseConsultantResponse(text, query.trim());
32
+ return buildReport({
33
+ agentId: "ai-consultant",
34
+ input: query.trim(),
35
+ riskScore: 0,
36
+ riskLevel: "LOW",
37
+ verdict: parsed.verdict,
38
+ keyFindings: parsed.keyFindings.map((f) => finding(f.label, f.value, f.impact, f.explanation)),
39
+ recommendations: parsed.recommendations,
40
+ confidence: "Medium — AI-generated guidance; verify with on-chain scans",
41
+ dataSource,
42
+ rawEvidence: parsed.rawEvidence,
43
+ ai_summary_available: true,
44
+ ai_summary_reason: null,
45
+ });
46
+ }
47
+ lastError = new Error("Empty AI response");
48
+ } catch (e) {
49
+ lastError = e;
50
+ logConsultantError(e, { attempt }, llmClient.model);
51
+ if (attempt < maxAttempts) await new Promise((r) => setTimeout(r, 800));
52
+ }
53
+ }
54
+
55
+ logConsultantError(lastError, { final: true }, llmClient.model);
56
+ throw new Error(`AI consultant unavailable: ${lastError?.message || "LLM request failed"}`);
57
+ }
@@ -0,0 +1,537 @@
1
+ /**
2
+ * Local premium agent runners — CLI BYOK / local execution without SolGuard backend.
3
+ * MongoDB-dependent agents (privacy vault, encrypted vault) are excluded.
4
+ */
5
+ import { Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";
6
+ import { runTokenScan } from "../scanEngine.js";
7
+ import { generateRiskSummary, isInvalidAiVerdict } from "../aiSummary.js";
8
+ import {
9
+ buildReport,
10
+ buildOnChainVerdict,
11
+ finding,
12
+ formatSol,
13
+ holderShareLabel,
14
+ } from "../reportBuilder.js";
15
+ import { analyzeOpenClawConfig } from "../openclawAudit.js";
16
+ import { EXPLOITS } from "../exploits.js";
17
+ import { runConsultant } from "./consultant.js";
18
+
19
+ const TOKEN_SOURCES = ["Helius RPC", "DexScreener", "Solana Mainnet"];
20
+ const WALLET_SOURCES = ["Helius RPC", "Solana Mainnet"];
21
+ const WEB_SOURCES = ["HTTP Security Scan"];
22
+ const CONFIG_SOURCES = ["Static JSON rule engine"];
23
+
24
+ /** Agent IDs that support premium local mode in the CLI. */
25
+ export const LOCAL_PREMIUM_AGENTS = {
26
+ "ai-consultant": { usesByok: true },
27
+ "solana-token-verification": { usesByok: true },
28
+ "contract-security": { usesByok: false },
29
+ "wallet-verification": { usesByok: false },
30
+ "website-security": { usesByok: false },
31
+ "openclaw-ai-agent-verification": { usesByok: false },
32
+ };
33
+
34
+ const HELIUS_AGENTS = new Set([
35
+ "solana-token-verification",
36
+ "contract-security",
37
+ "wallet-verification",
38
+ ]);
39
+
40
+ export function canRunLocalPremium(agentId) {
41
+ return agentId in LOCAL_PREMIUM_AGENTS;
42
+ }
43
+
44
+ export function agentUsesByokLlm(agentId) {
45
+ return LOCAL_PREMIUM_AGENTS[agentId]?.usesByok === true;
46
+ }
47
+
48
+ export function localPremiumRequiresHelius(agentId) {
49
+ return HELIUS_AGENTS.has(agentId);
50
+ }
51
+
52
+ function getConnection() {
53
+ const key = process.env.HELIUS_API_KEY;
54
+ if (!key) {
55
+ throw new Error(
56
+ "HELIUS_API_KEY is required for local on-chain scans. Set it in your environment before using premium local mode."
57
+ );
58
+ }
59
+ return new Connection(`https://mainnet.helius-rpc.com/?api-key=${key}`, "confirmed");
60
+ }
61
+
62
+ async function getWalletProfile(walletAddress) {
63
+ const conn = getConnection();
64
+ const pk = new PublicKey(walletAddress);
65
+ const lamports = await conn.getBalance(pk);
66
+ const balanceSol = lamports / LAMPORTS_PER_SOL;
67
+
68
+ let before = undefined;
69
+ let oldestSig = null;
70
+ let newestSig = null;
71
+ let txCountSampled = 0;
72
+ let historyComplete = true;
73
+ const MAX_PAGES = 15;
74
+ const PAGE = 1000;
75
+
76
+ for (let page = 0; page < MAX_PAGES; page++) {
77
+ const batch = await conn.getSignaturesForAddress(pk, { limit: PAGE, before });
78
+ if (!batch.length) break;
79
+ txCountSampled += batch.length;
80
+ if (!newestSig) newestSig = batch[0];
81
+ oldestSig = batch[batch.length - 1];
82
+ if (batch.length < PAGE) break;
83
+ before = batch[batch.length - 1].signature;
84
+ if (page === MAX_PAGES - 1) historyComplete = false;
85
+ }
86
+
87
+ const firstTxAt = oldestSig?.blockTime ?? null;
88
+ const lastTxAt = newestSig?.blockTime ?? null;
89
+ const ageDays = firstTxAt != null ? Math.floor((Date.now() / 1000 - firstTxAt) / 86400) : null;
90
+ const botFlag = txCountSampled > 30 && ageDays != null && ageDays < 1;
91
+
92
+ return {
93
+ lamports,
94
+ balanceSol,
95
+ balanceLabel: formatSol(lamports),
96
+ ageDays,
97
+ txCountSampled,
98
+ historyComplete,
99
+ firstTxAt,
100
+ lastTxAt,
101
+ botFlag,
102
+ };
103
+ }
104
+
105
+ function walletRecommendations(profile) {
106
+ const recs = [];
107
+ if (profile.ageDays != null && profile.ageDays < 7) {
108
+ recs.push("Verify the wallet owner's identity through an independent channel before sending significant funds.");
109
+ }
110
+ if (profile.txCountSampled < 5) {
111
+ recs.push("Request additional on-chain activity proof — this wallet has very few recorded transactions.");
112
+ }
113
+ if (profile.botFlag) {
114
+ recs.push("Review whether automated bots control this wallet before relying on its trading behavior.");
115
+ }
116
+ if (profile.balanceSol >= 1_000_000) {
117
+ recs.push("Treat as a high-value treasury or exchange hot wallet — confirm you are interacting with the intended counterparty.");
118
+ } else if (profile.balanceSol < 0.01) {
119
+ recs.push("Low SOL balance may indicate a disposable wallet — avoid high-value transfers until history improves.");
120
+ }
121
+ if (!recs.length) recs.push("No urgent actions required — continue monitoring for unusual outbound transfers.");
122
+ return recs.slice(0, 4);
123
+ }
124
+
125
+ function walletConfidence(profile) {
126
+ if (profile.historyComplete) {
127
+ return `High — full on-chain history scanned (${profile.txCountSampled.toLocaleString()} transactions)`;
128
+ }
129
+ return `Medium — sampled ${profile.txCountSampled.toLocaleString()} transactions; wallet may predate oldest scanned activity`;
130
+ }
131
+
132
+ function deriveTokenRecs(scan) {
133
+ const recs = [];
134
+ if (scan.authorityCheck.freezeAuthority === "ACTIVE") recs.push("Avoid trading until the freeze authority is permanently revoked.");
135
+ if (scan.authorityCheck.mintAuthority === "ACTIVE") recs.push("Monitor supply on explorers — the deployer can mint unlimited new tokens.");
136
+ if (scan.bundleDetection.detected) recs.push("Watch for coordinated sell pressure from the clustered launch wallets.");
137
+ if (!scan.liquidityLock.poolFound) recs.push("Do not buy until a verified DEX pool with adequate liquidity exists.");
138
+ if (scan.liquidityLock.poolFound && scan.liquidityLock.liquidityUsd != null && scan.liquidityLock.liquidityUsd < 10000) {
139
+ recs.push("Use small position sizes — shallow liquidity makes exits costly.");
140
+ }
141
+ if (!recs.length) recs.push("No critical flags — continue routine monitoring before increasing exposure.");
142
+ return recs.slice(0, 4);
143
+ }
144
+
145
+ function tokenConfidence(scan) {
146
+ const base = "High — mint account and liquidity data pulled from live mainnet RPC";
147
+ if (scan.bundleDetection?.holderDataAvailable === false) {
148
+ return `Medium — holder concentration unverified (${scan.bundleDetection.holderDataError || "RPC limitation"})`;
149
+ }
150
+ return `${base}; top-20 holder balances via getTokenLargestAccounts`;
151
+ }
152
+
153
+ function pickTokenVerdict(ai, scan) {
154
+ if (ai.aiAvailable && ai.verdict && !isInvalidAiVerdict(ai.verdict)) {
155
+ return { verdict: ai.verdict, aiUsed: true, aiReason: ai.reason };
156
+ }
157
+ const reason = ai.aiAvailable && ai.verdict ? "invalid_verdict_shape" : ai.reason;
158
+ return { verdict: buildOnChainVerdict(scan), aiUsed: false, aiReason: reason };
159
+ }
160
+
161
+ function tokenFindingsFromScan(scan) {
162
+ const a = scan.authorityCheck;
163
+ const b = scan.bundleDetection;
164
+ const l = scan.liquidityLock;
165
+ return [
166
+ finding(
167
+ "Mint Authority",
168
+ a.mintAuthority === "ACTIVE" ? "Active" : "Revoked",
169
+ a.mintAuthority === "ACTIVE" ? "+20 risk" : "-20 risk",
170
+ a.mintAuthority === "ACTIVE"
171
+ ? "The deployer can create unlimited new tokens, diluting existing holders at any time."
172
+ : "No one can mint new supply, which removes a common rug-pull vector."
173
+ ),
174
+ finding(
175
+ "Freeze Authority",
176
+ a.freezeAuthority === "ACTIVE" ? "Active" : "Revoked",
177
+ a.freezeAuthority === "ACTIVE" ? "+30 risk" : "neutral",
178
+ a.freezeAuthority === "ACTIVE"
179
+ ? "The deployer can freeze any holder's tokens, locking them out of selling."
180
+ : "Holder wallets cannot be frozen by the token creator."
181
+ ),
182
+ finding(
183
+ "Top Holder Concentration",
184
+ holderShareLabel(b, false) + (b.holderDataAvailable ? " of supply" : ""),
185
+ !b.holderDataAvailable ? "unknown" : b.topHolderPercent > 20 ? "+10 risk" : "neutral",
186
+ !b.holderDataAvailable
187
+ ? "Concentration could not be measured — do not assume a safe distribution."
188
+ : b.topHolderPercent > 20
189
+ ? "A single wallet controls a large share, increasing dump risk."
190
+ : "No single wallet dominates supply, which supports healthier distribution."
191
+ ),
192
+ finding(
193
+ "Top 10 Holder Concentration",
194
+ holderShareLabel(b, true) + (b.holderDataAvailable ? " of supply" : ""),
195
+ !b.holderDataAvailable ? "unknown" : b.top10Percent > 50 ? "+25 risk" : "neutral",
196
+ !b.holderDataAvailable
197
+ ? "Top-10 share unknown — high-volume tokens may exceed RPC index limits."
198
+ : b.top10Percent > 50
199
+ ? "A small group could coordinate a mass sell-off."
200
+ : "Supply is spread across many holders rather than a tight cluster."
201
+ ),
202
+ finding(
203
+ "Launch Bundle Clustering",
204
+ b.detected ? `${b.walletCount} wallets clustered` : "Not detected",
205
+ b.detected ? "+25 risk" : "neutral",
206
+ b.detected
207
+ ? "Many wallets bought in the same block window, suggesting coordinated sniping."
208
+ : "No abnormal launch-time wallet clustering was detected."
209
+ ),
210
+ finding(
211
+ "DEX Liquidity",
212
+ l.poolFound ? `$${Math.round(l.liquidityUsd || 0).toLocaleString("en-US")} USD` : "No pool found",
213
+ !l.poolFound ? "+5 risk" : l.liquidityUsd != null && l.liquidityUsd < 10000 ? "+15 risk" : "neutral",
214
+ l.poolFound
215
+ ? l.liquidityUsd != null && l.liquidityUsd < 10000
216
+ ? "Thin liquidity means large sells will move the price sharply."
217
+ : "An active pool exists, making exits more feasible."
218
+ : "Without a DEX pool, selling may be impossible or extremely costly."
219
+ ),
220
+ ];
221
+ }
222
+
223
+ async function runTokenAuditLocal({ tokenAddress }, { llmClient, dataSource }) {
224
+ const scan = await runTokenScan(tokenAddress);
225
+ const ai = await generateRiskSummary(scan, { llmClient });
226
+ const picked = pickTokenVerdict(ai, scan);
227
+ return buildReport({
228
+ agentId: "token-audit",
229
+ input: tokenAddress,
230
+ riskScore: scan.riskScore,
231
+ riskLevel: scan.riskLevel,
232
+ verdict: picked.verdict,
233
+ keyFindings: tokenFindingsFromScan(scan),
234
+ recommendations: deriveTokenRecs(scan),
235
+ confidence: picked.aiUsed ? `${tokenConfidence(scan)}; AI verdict generated` : `${tokenConfidence(scan)}; AI unavailable, on-chain verdict used`,
236
+ dataSource: dataSource || TOKEN_SOURCES,
237
+ scannedAt: scan.scannedAt,
238
+ rawEvidence: { ...scan, ai_summary_available: picked.aiUsed, ai_summary_reason: picked.aiReason },
239
+ ai_summary_available: picked.aiUsed,
240
+ ai_summary_reason: picked.aiReason,
241
+ });
242
+ }
243
+
244
+ async function runSolanaTokenVerificationLocal(inputs, opts) {
245
+ const { tokenAddress } = inputs;
246
+ const audit = await runTokenAuditLocal({ tokenAddress }, opts);
247
+ const scan = audit.rawEvidence;
248
+ const b = scan.bundleDetection;
249
+ return {
250
+ ...audit,
251
+ agentId: "solana-token-verification",
252
+ rawEvidence: {
253
+ ...scan,
254
+ ai_summary_available: audit.ai_summary_available,
255
+ ai_summary_reason: audit.ai_summary_reason,
256
+ subModules: {
257
+ bundle: scan.bundleDetection,
258
+ holders: { topHolderPercent: b.topHolderPercent, top10Percent: b.top10Percent, riskFlags: b.riskFlags || [] },
259
+ liquidity: scan.liquidityLock,
260
+ },
261
+ },
262
+ evidence: {
263
+ ...scan,
264
+ ai_summary_available: audit.ai_summary_available,
265
+ ai_summary_reason: audit.ai_summary_reason,
266
+ subModules: {
267
+ bundle: scan.bundleDetection,
268
+ holders: { topHolderPercent: b.topHolderPercent, top10Percent: b.top10Percent, riskFlags: b.riskFlags || [] },
269
+ liquidity: scan.liquidityLock,
270
+ },
271
+ },
272
+ };
273
+ }
274
+
275
+ async function runContractSecurityLocal({ tokenAddress }) {
276
+ const scan = await runTokenScan(tokenAddress);
277
+ const a = scan.authorityCheck;
278
+ const score = (a.mintAuthority === "ACTIVE" ? 50 : 0) + (a.freezeAuthority === "ACTIVE" ? 50 : 0);
279
+ const level = score >= 50 ? "HIGH" : score >= 20 ? "MEDIUM" : "LOW";
280
+ const verdict = score === 0
281
+ ? "Both mint and freeze authorities are revoked, so the deployer cannot mint new tokens or freeze holder wallets."
282
+ : score >= 50
283
+ ? "Active mint or freeze authorities give the deployer direct control to inflate supply or lock holder wallets."
284
+ : "One authority remains active, leaving a partial rug-pull vector on this token contract.";
285
+
286
+ return buildReport({
287
+ agentId: "contract-security",
288
+ input: tokenAddress,
289
+ riskScore: score,
290
+ riskLevel: level,
291
+ verdict,
292
+ keyFindings: [
293
+ finding(
294
+ "Mint Authority",
295
+ a.mintAuthority === "ACTIVE" ? `Active (${a.mintAuthorityAddress || "unknown"})` : "Revoked",
296
+ a.mintAuthority === "ACTIVE" ? "+50 risk" : "neutral",
297
+ a.mintAuthority === "ACTIVE"
298
+ ? "The token creator can mint unlimited new tokens at any time."
299
+ : "New tokens cannot be minted, removing inflation risk from the deployer."
300
+ ),
301
+ finding(
302
+ "Freeze Authority",
303
+ a.freezeAuthority === "ACTIVE" ? `Active (${a.freezeAuthorityAddress || "unknown"})` : "Revoked",
304
+ a.freezeAuthority === "ACTIVE" ? "+50 risk" : "neutral",
305
+ a.freezeAuthority === "ACTIVE"
306
+ ? "The deployer can freeze any wallet holding this token."
307
+ : "Holder balances cannot be frozen by the token creator."
308
+ ),
309
+ ],
310
+ recommendations: score === 0
311
+ ? ["Proceed with standard due diligence on holders and liquidity — contract authorities are safely locked."]
312
+ : [
313
+ ...(a.mintAuthority === "ACTIVE" ? ["Do not buy until mint authority is permanently revoked on-chain."] : []),
314
+ ...(a.freezeAuthority === "ACTIVE" ? ["Avoid holding until freeze authority is revoked — your wallet could be frozen."] : []),
315
+ "Verify authority status yourself on Solscan before making any trade.",
316
+ ].slice(0, 4),
317
+ confidence: tokenConfidence(scan),
318
+ dataSource: TOKEN_SOURCES,
319
+ scannedAt: scan.scannedAt,
320
+ rawEvidence: {
321
+ mintAuthority: a.mintAuthority,
322
+ freezeAuthority: a.freezeAuthority,
323
+ mintAuthorityAddress: a.mintAuthorityAddress,
324
+ freezeAuthorityAddress: a.freezeAuthorityAddress,
325
+ },
326
+ });
327
+ }
328
+
329
+ async function runWalletAuditLocal({ walletAddress }) {
330
+ const profile = await getWalletProfile(walletAddress);
331
+ const score = Math.min(
332
+ 100,
333
+ (profile.ageDays != null && profile.ageDays < 7 ? 40 : profile.ageDays != null && profile.ageDays < 30 ? 20 : 0)
334
+ + (profile.txCountSampled < 5 ? 30 : 0)
335
+ + (profile.botFlag ? 20 : 0)
336
+ );
337
+ const level = score >= 50 ? "HIGH" : score >= 20 ? "MEDIUM" : "LOW";
338
+ const ageLabel = profile.ageDays != null ? `~${profile.ageDays} days since earliest scanned transaction` : "unknown activity span";
339
+ const verdict = profile.botFlag
340
+ ? `This wallet shows high-frequency activity within its first day and holds ${profile.balanceLabel}, which warrants extra scrutiny before trusting it.`
341
+ : `This wallet holds ${profile.balanceLabel} with ${profile.txCountSampled.toLocaleString()} transactions sampled and earliest activity ${ageLabel}.`;
342
+
343
+ const keyFindings = [
344
+ finding(
345
+ "Current SOL Balance",
346
+ profile.balanceLabel,
347
+ profile.balanceSol >= 1_000_000 ? "+10 risk" : "neutral",
348
+ profile.balanceSol >= 1_000_000
349
+ ? "Very large balances usually belong to exchanges or treasuries — confirm counterparty identity."
350
+ : "Native SOL balance reflects funds available for fees and transfers on Solana mainnet."
351
+ ),
352
+ finding(
353
+ "Account Activity Span",
354
+ ageLabel,
355
+ profile.ageDays != null && profile.ageDays < 7 ? "+40 risk" : profile.ageDays != null && profile.ageDays < 30 ? "+20 risk" : "neutral",
356
+ profile.ageDays != null && profile.ageDays < 7
357
+ ? "Very recent first activity means limited track record for trust decisions."
358
+ : "Longer activity history provides more behavioral data to assess legitimacy."
359
+ ),
360
+ finding(
361
+ "Transactions Sampled",
362
+ profile.txCountSampled.toLocaleString(),
363
+ profile.txCountSampled < 5 ? "+30 risk" : "neutral",
364
+ profile.txCountSampled < 5
365
+ ? "Minimal history makes it hard to distinguish legitimate users from throwaway wallets."
366
+ : "Sufficient transaction volume exists to profile typical wallet behavior."
367
+ ),
368
+ ];
369
+ if (profile.botFlag) {
370
+ keyFindings.push(finding("Bot Heuristic", "High-frequency new wallet", "+20 risk", "Burst activity on a brand-new wallet often indicates automated trading bots."));
371
+ }
372
+
373
+ return buildReport({
374
+ agentId: "wallet-audit",
375
+ input: walletAddress,
376
+ riskScore: score,
377
+ riskLevel: level,
378
+ verdict,
379
+ keyFindings,
380
+ recommendations: walletRecommendations(profile),
381
+ confidence: walletConfidence(profile),
382
+ dataSource: WALLET_SOURCES,
383
+ rawEvidence: {
384
+ lamports: profile.lamports,
385
+ balanceSol: profile.balanceSol,
386
+ balanceLabel: profile.balanceLabel,
387
+ ageDays: profile.ageDays,
388
+ txCountSampled: profile.txCountSampled,
389
+ historyComplete: profile.historyComplete,
390
+ firstTxAt: profile.firstTxAt,
391
+ lastTxAt: profile.lastTxAt,
392
+ botFlag: profile.botFlag,
393
+ },
394
+ });
395
+ }
396
+
397
+ async function runWalletVerificationLocal({ walletAddress }) {
398
+ const base = await runWalletAuditLocal({ walletAddress });
399
+ const linked = EXPLOITS.filter((e) => (e.relatedWallets || []).some((w) => w === walletAddress));
400
+ let score = base.riskScore;
401
+ let level = base.riskLevel;
402
+ const keyFindings = [...base.keyFindings];
403
+ const recommendations = [...base.recommendations];
404
+
405
+ if (linked.length) {
406
+ keyFindings.push(finding(
407
+ "Exploit Database Match",
408
+ linked.map((e) => e.project).join(", "),
409
+ "+40 risk",
410
+ "This wallet address appears in SolGuard's known exploit incident records."
411
+ ));
412
+ score = Math.min(100, score + 40);
413
+ if (score >= 50) level = "HIGH";
414
+ else if (score >= 20 && level === "LOW") level = "MEDIUM";
415
+ recommendations.unshift("Do not send funds — cross-reference this wallet on Solscan and block if tied to known exploits.");
416
+ }
417
+
418
+ const verdict = linked.length
419
+ ? `${base.verdict} This wallet also matches ${linked.length} known exploit incident(s) in our database.`
420
+ : base.verdict;
421
+
422
+ const rawEvidence = {
423
+ ...base.rawEvidence,
424
+ exploitMatches: linked.map(({ id, project, vector, date, lossUsd }) => ({ id, project, vector, date, lossUsd })),
425
+ };
426
+
427
+ return buildReport({
428
+ ...base,
429
+ agentId: "wallet-verification",
430
+ verdict,
431
+ summary: verdict,
432
+ riskScore: score,
433
+ riskLevel: level,
434
+ keyFindings,
435
+ recommendations: recommendations.slice(0, 4),
436
+ rawEvidence,
437
+ evidence: rawEvidence,
438
+ });
439
+ }
440
+
441
+ async function runWebsiteSecurityLocal({ url }) {
442
+ try {
443
+ const res = await fetch(url, { method: "GET", redirect: "follow" });
444
+ const headers = Object.fromEntries(res.headers);
445
+ const flags = [];
446
+ if (!url.startsWith("https://")) flags.push("Site not served over HTTPS");
447
+ if (!headers["strict-transport-security"]) flags.push("Missing HSTS header");
448
+ if (!headers["content-security-policy"]) flags.push("Missing Content-Security-Policy header");
449
+ if (!headers["x-frame-options"]) flags.push("Missing X-Frame-Options");
450
+ const score = Math.min(80, flags.length * 15);
451
+ const level = score >= 45 ? "HIGH" : score >= 20 ? "MEDIUM" : "LOW";
452
+ const verdict = flags.length
453
+ ? `The site responded with HTTP ${res.status} but is missing ${flags.length} core security header(s), increasing phishing and injection risk.`
454
+ : `The site responded with HTTP ${res.status} and has core security headers configured.`;
455
+
456
+ return buildReport({
457
+ agentId: "website-security",
458
+ input: url,
459
+ riskScore: score,
460
+ riskLevel: level,
461
+ verdict,
462
+ keyFindings: [
463
+ finding("HTTPS", url.startsWith("https://") ? "Enabled" : "Not used", !url.startsWith("https://") ? "+15 risk" : "neutral", !url.startsWith("https://") ? "Unencrypted connections expose users to interception." : "Traffic is encrypted in transit."),
464
+ finding("HSTS", headers["strict-transport-security"] ? "Present" : "Missing", !headers["strict-transport-security"] ? "+15 risk" : "neutral", !headers["strict-transport-security"] ? "Browsers may fall back to insecure HTTP without HSTS." : "HSTS helps enforce HTTPS connections."),
465
+ finding("Content-Security-Policy", headers["content-security-policy"] ? "Present" : "Missing", !headers["content-security-policy"] ? "+15 risk" : "neutral", !headers["content-security-policy"] ? "Missing CSP increases XSS attack surface." : "CSP restricts untrusted script execution."),
466
+ finding("X-Frame-Options", headers["x-frame-options"] ? "Present" : "Missing", !headers["x-frame-options"] ? "+15 risk" : "neutral", !headers["x-frame-options"] ? "Site may be embeddable in clickjacking frames." : "Clickjacking protection is enabled."),
467
+ ],
468
+ recommendations: flags.length
469
+ ? flags.map((f) => `Ask the site operator to fix: ${f}.`)
470
+ : ["Security headers look adequate — continue verifying smart contract and token risks separately."],
471
+ confidence: "Medium — single HTTP response snapshot; headers may vary by route",
472
+ dataSource: WEB_SOURCES,
473
+ rawEvidence: { status: res.status, headers, flags },
474
+ });
475
+ } catch (e) {
476
+ return buildReport({
477
+ agentId: "website-security",
478
+ input: url,
479
+ riskScore: 60,
480
+ riskLevel: "HIGH",
481
+ verdict: `The site at ${url} could not be reached, which is a red flag for project legitimacy.`,
482
+ keyFindings: [finding("Reachability", "Failed", "+60 risk", "Unreachable sites often indicate scams or abandoned projects.")],
483
+ recommendations: ["Do not connect your wallet to this site until it is reachable over HTTPS.", "Verify the official domain through the project's social channels."],
484
+ confidence: "Low — connection failed before headers could be read",
485
+ dataSource: WEB_SOURCES,
486
+ rawEvidence: { error: e.message },
487
+ });
488
+ }
489
+ }
490
+
491
+ async function runOpenClawLocal({ config }) {
492
+ const analysis = analyzeOpenClawConfig(config);
493
+ if (analysis.error) return { error: analysis.error };
494
+ return buildReport({
495
+ agentId: "openclaw-ai-agent-verification",
496
+ input: config.slice(0, 120) + (config.length > 120 ? "…" : ""),
497
+ riskScore: analysis.riskScore,
498
+ riskLevel: analysis.riskLevel,
499
+ verdict: analysis.verdict,
500
+ keyFindings: analysis.keyFindings,
501
+ recommendations: analysis.recommendations,
502
+ confidence: analysis.confidence,
503
+ dataSource: CONFIG_SOURCES,
504
+ rawEvidence: {
505
+ agentName: analysis.config?.name || null,
506
+ gatewayAuth: analysis.config?.gateway?.auth ?? analysis.config?.auth ?? null,
507
+ tools: analysis.config?.tools || [],
508
+ skills: analysis.config?.skills || [],
509
+ auditType: "rule-based-static-analysis",
510
+ },
511
+ });
512
+ }
513
+
514
+ /**
515
+ * Run an agent locally (CLI premium mode).
516
+ * @param {string} agentId
517
+ * @param {object} inputs
518
+ * @param {{ llmClient?: object, dataSource?: string[] }} opts
519
+ */
520
+ export async function runAgentLocal(agentId, inputs, { llmClient, dataSource } = {}) {
521
+ switch (agentId) {
522
+ case "ai-consultant":
523
+ return runConsultant(inputs, { llmClient, dataSource });
524
+ case "solana-token-verification":
525
+ return runSolanaTokenVerificationLocal(inputs, { llmClient, dataSource });
526
+ case "contract-security":
527
+ return runContractSecurityLocal(inputs);
528
+ case "wallet-verification":
529
+ return runWalletVerificationLocal(inputs);
530
+ case "website-security":
531
+ return runWebsiteSecurityLocal(inputs);
532
+ case "openclaw-ai-agent-verification":
533
+ return runOpenClawLocal(inputs);
534
+ default:
535
+ throw new Error(`Agent "${agentId}" does not support premium local mode`);
536
+ }
537
+ }