solidity-argus 0.2.0 → 0.3.2

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 (169) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +34 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +34 -7
  75. package/src/agents/pythia-prompt.ts +13 -4
  76. package/src/agents/scribe-prompt.ts +20 -2
  77. package/src/agents/sentinel-prompt.ts +45 -5
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +6 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/system-prompt-hook.ts +18 -1
  114. package/src/hooks/tool-tracking-hook.ts +110 -51
  115. package/src/hooks/types.ts +2 -1
  116. package/src/index.ts +24 -37
  117. package/src/knowledge/retry.ts +22 -22
  118. package/src/knowledge/scvd-client.ts +88 -95
  119. package/src/knowledge/scvd-errors.ts +35 -35
  120. package/src/knowledge/scvd-index.ts +78 -80
  121. package/src/knowledge/scvd-sync.ts +106 -101
  122. package/src/managers/index.ts +1 -1
  123. package/src/managers/types.ts +19 -14
  124. package/src/plugin-interface.ts +7 -9
  125. package/src/shared/binary-utils.ts +44 -35
  126. package/src/shared/deep-merge.ts +55 -36
  127. package/src/shared/file-utils.ts +21 -19
  128. package/src/shared/index.ts +11 -5
  129. package/src/shared/jsonc-parser.ts +123 -28
  130. package/src/shared/logger.ts +16 -3
  131. package/src/shared/project-utils.ts +30 -0
  132. package/src/skills/analysis/cluster.ts +414 -0
  133. package/src/skills/analysis/gates.ts +227 -0
  134. package/src/skills/analysis/index.ts +33 -0
  135. package/src/skills/analysis/normalize.ts +217 -0
  136. package/src/skills/analysis/similarity.ts +224 -0
  137. package/src/skills/argus-skill-resolver.ts +17 -6
  138. package/src/skills/skill-schema.ts +11 -10
  139. package/src/solodit-lifecycle.ts +203 -0
  140. package/src/state/audit-state.ts +8 -8
  141. package/src/state/finding-store.ts +68 -55
  142. package/src/state/types.ts +88 -67
  143. package/src/tools/argus-skill-load-tool.ts +12 -7
  144. package/src/tools/contract-analyzer-tool.ts +142 -77
  145. package/src/tools/forge-coverage-tool.ts +226 -0
  146. package/src/tools/forge-fuzz-tool.ts +127 -127
  147. package/src/tools/forge-test-tool.ts +201 -158
  148. package/src/tools/gas-analysis-tool.ts +264 -0
  149. package/src/tools/pattern-checker-tool.ts +203 -191
  150. package/src/tools/pattern-loader.ts +5 -111
  151. package/src/tools/pattern-schema.ts +3 -0
  152. package/src/tools/proxy-detection-tool.ts +224 -0
  153. package/src/tools/report-generator-tool.ts +305 -206
  154. package/src/tools/slither-tool.ts +266 -218
  155. package/src/tools/solodit-search-tool.ts +235 -119
  156. package/src/tools/sync-knowledge-tool.ts +7 -11
  157. package/src/utils/audit-artifact-detector.ts +28 -29
  158. package/src/utils/dependency-scanner.ts +37 -37
  159. package/src/utils/project-detector.ts +111 -124
  160. package/src/utils/solidity-parser.ts +175 -75
  161. package/skills/patterns/access-control.yaml +0 -31
  162. package/skills/patterns/erc4626.yaml +0 -29
  163. package/skills/patterns/flash-loan.yaml +0 -20
  164. package/skills/patterns/oracle.yaml +0 -30
  165. package/skills/patterns/proxy.yaml +0 -30
  166. package/skills/patterns/reentrancy.yaml +0 -30
  167. package/skills/patterns/signature.yaml +0 -31
  168. package/src/hooks/event-hook-v2.ts +0 -99
  169. package/src/state/plugin-state.ts +0 -14
@@ -1,43 +1,48 @@
1
- import { tool, type ToolContext } from "@opencode-ai/plugin";
1
+ import type { ToolDefinition } from "@opencode-ai/plugin"
2
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
3
+ import { createLogger } from "../shared/logger"
4
+ import { soloditAvailable } from "../solodit-lifecycle"
2
5
 
3
- const SOLODIT_MCP_SERVER = "solodit-mcp";
4
- const SOLODIT_MCP_TOOL = "search_findings";
5
- const DEFAULT_LIMIT = 10;
6
- const DEFAULT_SOLODIT_PORT = 3000;
7
- const SOLODIT_HTTP_TIMEOUT_MS = 10_000;
6
+ const logger = createLogger()
7
+
8
+ const SOLODIT_MCP_SERVER = "solodit-mcp"
9
+ const SOLODIT_MCP_TOOLS = ["search", "search_findings"] as const
10
+ const DEFAULT_LIMIT = 10
11
+ const DEFAULT_SOLODIT_PORT = 3000
12
+ const SOLODIT_HTTP_TIMEOUT_MS = 10_000
8
13
 
9
14
  type SoloditSearchArgs = {
10
- query: string;
11
- severity?: string[];
12
- limit?: number;
13
- };
15
+ query: string
16
+ severity?: string[]
17
+ limit?: number
18
+ }
14
19
 
15
20
  type SoloditFinding = {
16
- title: string;
17
- severity: string;
18
- description: string;
19
- protocol: string;
20
- url: string;
21
- remediation: string;
22
- };
21
+ title: string
22
+ severity: string
23
+ description: string
24
+ protocol: string
25
+ url: string
26
+ remediation: string
27
+ }
23
28
 
24
29
  export type SoloditSearchResult = {
25
- results: SoloditFinding[];
26
- totalFound: number;
27
- query: string;
28
- error?: string;
29
- };
30
+ results: SoloditFinding[]
31
+ totalFound: number
32
+ query: string
33
+ error?: string
34
+ }
30
35
 
31
36
  export type CallMcpTool = (
32
37
  server: string,
33
38
  tool: string,
34
- args: Record<string, unknown>
35
- ) => Promise<unknown>;
39
+ args: Record<string, unknown>,
40
+ ) => Promise<unknown>
36
41
 
37
- type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool };
42
+ type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
38
43
 
39
44
  function hasMcpCapability(ctx: ToolContext): ctx is McpCapableContext {
40
- return "callMcpTool" in ctx;
45
+ return "callMcpTool" in ctx
41
46
  }
42
47
 
43
48
  function parseFinding(raw: unknown): SoloditFinding {
@@ -49,161 +54,272 @@ function parseFinding(raw: unknown): SoloditFinding {
49
54
  protocol: "",
50
55
  url: "",
51
56
  remediation: "",
52
- };
57
+ }
53
58
  }
54
59
 
55
- const obj = raw as Record<string, unknown>;
60
+ const obj = raw as Record<string, unknown>
56
61
  return {
57
- title: typeof obj["title"] === "string" ? obj["title"] : "",
58
- severity: typeof obj["severity"] === "string" ? obj["severity"] : "",
59
- description: typeof obj["description"] === "string" ? obj["description"] : "",
60
- protocol: typeof obj["protocol"] === "string" ? obj["protocol"] : "",
61
- url: typeof obj["url"] === "string" ? obj["url"] : "",
62
- remediation: typeof obj["remediation"] === "string" ? obj["remediation"] : "",
63
- };
62
+ title: typeof obj.title === "string" ? obj.title : "",
63
+ severity: typeof obj.severity === "string" ? obj.severity : "",
64
+ description: typeof obj.description === "string" ? obj.description : "",
65
+ protocol: typeof obj.protocol === "string" ? obj.protocol : "",
66
+ url: typeof obj.url === "string" ? obj.url : "",
67
+ remediation: typeof obj.remediation === "string" ? obj.remediation : "",
68
+ }
64
69
  }
65
70
 
66
71
  function parseFindings(response: unknown): SoloditFinding[] {
67
72
  if (!Array.isArray(response)) {
68
- return [];
73
+ return []
74
+ }
75
+ return response.map(parseFinding)
76
+ }
77
+
78
+ function parseFindingsFromAnyResponse(response: unknown): SoloditFinding[] {
79
+ const direct = parseFindings(response)
80
+ if (direct.length > 0) return direct
81
+
82
+ if (typeof response === "object" && response !== null) {
83
+ const findings = (response as Record<string, unknown>).findings
84
+ if (Array.isArray(findings)) return findings.map(parseFinding)
69
85
  }
70
- return response.map(parseFinding);
86
+
87
+ return extractFindingsFromMcpResponse(response)
88
+ }
89
+
90
+ function hasMcpError(response: unknown): boolean {
91
+ if (typeof response !== "object" || response === null) return false
92
+ const obj = response as Record<string, unknown>
93
+ return "error" in obj
94
+ }
95
+
96
+ function normalizeImpacts(
97
+ severity?: string[],
98
+ ): Array<"HIGH" | "MEDIUM" | "LOW" | "GAS"> | undefined {
99
+ if (!severity || severity.length === 0) return undefined
100
+ const allowed = new Set(["HIGH", "MEDIUM", "LOW", "GAS"] as const)
101
+ const impacts = severity
102
+ .map((s) => s.toUpperCase())
103
+ .filter((s): s is "HIGH" | "MEDIUM" | "LOW" | "GAS" =>
104
+ allowed.has(s as "HIGH" | "MEDIUM" | "LOW" | "GAS"),
105
+ )
106
+ return impacts.length > 0 ? impacts : undefined
107
+ }
108
+
109
+ function buildMcpArgs(
110
+ toolName: (typeof SOLODIT_MCP_TOOLS)[number],
111
+ query: string,
112
+ limit: number,
113
+ severity?: string[],
114
+ ): Record<string, unknown> {
115
+ if (toolName === "search") {
116
+ return { keywords: query }
117
+ }
118
+
119
+ const impact = normalizeImpacts(severity)
120
+ return {
121
+ keywords: query,
122
+ ...(impact ? { impact } : {}),
123
+ pageSize: limit,
124
+ }
125
+ }
126
+
127
+ function filterFindingsBySeverity(
128
+ findings: SoloditFinding[],
129
+ severities?: string[],
130
+ ): SoloditFinding[] {
131
+ if (!severities || severities.length === 0) return findings
132
+
133
+ const allowed = new Set(severities.map((s) => s.toLowerCase()))
134
+ return findings.filter((finding) => allowed.has(finding.severity.toLowerCase()))
71
135
  }
72
136
 
73
137
  function parseSseData(body: string): unknown {
74
138
  for (const line of body.split("\n")) {
75
139
  if (line.startsWith("data: ")) {
76
140
  try {
77
- return JSON.parse(line.slice(6));
78
- } catch {
79
- continue;
80
- }
141
+ return JSON.parse(line.slice(6))
142
+ } catch {}
81
143
  }
82
144
  }
83
145
  try {
84
- return JSON.parse(body);
146
+ return JSON.parse(body)
85
147
  } catch {
86
- return null;
148
+ return null
87
149
  }
88
150
  }
89
151
 
90
152
  function extractFindingsFromMcpResponse(envelope: unknown): SoloditFinding[] {
91
- if (typeof envelope !== "object" || envelope === null) return [];
92
- const result = (envelope as Record<string, unknown>).result;
93
- if (typeof result !== "object" || result === null) return [];
153
+ if (typeof envelope !== "object" || envelope === null) return []
154
+ const result = (envelope as Record<string, unknown>).result
155
+ if (typeof result !== "object" || result === null) return []
94
156
 
95
- const structured = (result as Record<string, unknown>).structuredContent;
157
+ const structured = (result as Record<string, unknown>).structuredContent
96
158
  const reportsJson =
97
159
  typeof structured === "object" && structured !== null
98
160
  ? (structured as Record<string, unknown>).reportsJSON
99
- : undefined;
161
+ : undefined
100
162
 
101
163
  if (typeof reportsJson === "string") {
102
164
  try {
103
- const parsed = JSON.parse(reportsJson);
104
- if (Array.isArray(parsed)) return parsed.map(parseFinding);
105
- } catch { /* fall through */ }
165
+ const parsed = JSON.parse(reportsJson)
166
+ if (Array.isArray(parsed)) return parsed.map(parseFinding)
167
+ } catch {
168
+ logger.debug("Failed to parse Solodit structured response")
169
+ }
106
170
  }
107
171
 
108
- const content = (result as Record<string, unknown>).content;
172
+ const content = (result as Record<string, unknown>).content
109
173
  if (Array.isArray(content) && content.length > 0) {
110
- const first = content[0] as Record<string, unknown> | undefined;
174
+ const first = content[0] as Record<string, unknown> | undefined
111
175
  if (typeof first?.text === "string") {
112
176
  try {
113
- const parsed = JSON.parse(first.text);
114
- if (Array.isArray(parsed)) return parsed.map(parseFinding);
115
- } catch { /* fall through */ }
177
+ const parsed = JSON.parse(first.text)
178
+ if (Array.isArray(parsed)) return parsed.map(parseFinding)
179
+ } catch {
180
+ logger.debug("Failed to parse Solodit content text")
181
+ }
116
182
  }
117
183
  }
118
184
 
119
- return [];
185
+ return []
120
186
  }
121
187
 
122
188
  async function callSoloditHttp(
123
189
  query: string,
124
190
  limit: number,
191
+ severities?: string[],
125
192
  port: number = DEFAULT_SOLODIT_PORT,
126
193
  ): Promise<SoloditSearchResult> {
127
- try {
128
- const response = await fetch(`http://localhost:${port}/mcp`, {
129
- method: "POST",
130
- headers: {
131
- "Content-Type": "application/json",
132
- Accept: "application/json, text/event-stream",
133
- },
134
- body: JSON.stringify({
135
- jsonrpc: "2.0",
136
- method: "tools/call",
137
- params: { name: "search", arguments: { keywords: query } },
138
- id: 1,
139
- }),
140
- signal: AbortSignal.timeout(SOLODIT_HTTP_TIMEOUT_MS),
141
- });
142
-
143
- if (!response.ok) {
144
- return { results: [], totalFound: 0, query, error: `Solodit HTTP ${response.status}` };
145
- }
194
+ let lastError: string | undefined
195
+
196
+ for (const toolName of SOLODIT_MCP_TOOLS) {
197
+ try {
198
+ const response = await fetch(`http://localhost:${port}/mcp`, {
199
+ method: "POST",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ Accept: "application/json, text/event-stream",
203
+ },
204
+ body: JSON.stringify({
205
+ jsonrpc: "2.0",
206
+ method: "tools/call",
207
+ params: { name: toolName, arguments: buildMcpArgs(toolName, query, limit, severities) },
208
+ id: 1,
209
+ }),
210
+ signal: AbortSignal.timeout(SOLODIT_HTTP_TIMEOUT_MS),
211
+ })
146
212
 
147
- const body = await response.text();
148
- const envelope = parseSseData(body);
149
- const findings = extractFindingsFromMcpResponse(envelope);
213
+ if (!response.ok) {
214
+ lastError = `Solodit HTTP ${response.status}`
215
+ continue
216
+ }
217
+
218
+ const body = await response.text()
219
+ const envelope = parseSseData(body)
220
+
221
+ if (hasMcpError(envelope)) {
222
+ continue
223
+ }
150
224
 
151
- return { results: findings.slice(0, limit), totalFound: findings.length, query };
152
- } catch (error) {
153
- const message = error instanceof Error ? error.message : "Unknown error";
154
- return { results: [], totalFound: 0, query, error: `Solodit MCP unreachable: ${message}` };
225
+ const findings = filterFindingsBySeverity(parseFindingsFromAnyResponse(envelope), severities)
226
+
227
+ return { results: findings.slice(0, limit), totalFound: findings.length, query }
228
+ } catch (error) {
229
+ const message = error instanceof Error ? error.message : "Unknown error"
230
+ lastError = `Solodit MCP unreachable: ${message}`
231
+ }
155
232
  }
233
+
234
+ return { results: [], totalFound: 0, query, error: lastError ?? "Solodit MCP call failed" }
156
235
  }
157
236
 
158
237
  export async function executeSoloditSearch(
159
238
  args: SoloditSearchArgs,
160
239
  context: ToolContext,
161
- callMcpTool?: CallMcpTool
240
+ callMcpTool?: CallMcpTool,
241
+ port: number = DEFAULT_SOLODIT_PORT,
162
242
  ): Promise<SoloditSearchResult> {
163
- const { query } = args;
164
- const limit = args.limit ?? DEFAULT_LIMIT;
243
+ const { query } = args
244
+ const limit = args.limit ?? DEFAULT_LIMIT
165
245
 
166
- context.metadata({ title: `Solodit search: ${query}` });
246
+ context.metadata({ title: `Solodit search: ${query}` })
247
+
248
+ // Belt-and-suspenders: check if Solodit MCP is available, with 3s retry
249
+ // Skip check in test environment
250
+ if (!soloditAvailable && process.env.NODE_ENV !== "test") {
251
+ // Wait up to 3s for monitoring to flip the flag
252
+ for (let i = 0; i < 3 && !soloditAvailable; i++) {
253
+ await Bun.sleep(1000)
254
+ }
255
+ if (!soloditAvailable) {
256
+ return {
257
+ results: [],
258
+ totalFound: 0,
259
+ query,
260
+ error:
261
+ "Solodit MCP not available — server did not start. Results limited to local patterns.",
262
+ }
263
+ }
264
+ }
167
265
 
168
- const mcpCaller =
169
- callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined);
266
+ const mcpCaller = callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined)
170
267
 
171
268
  if (!mcpCaller) {
172
- return callSoloditHttp(query, limit);
269
+ return callSoloditHttp(query, limit, args.severity, port)
173
270
  }
174
271
 
175
- try {
176
- const mcpArgs: Record<string, unknown> = { query, limit };
272
+ let hadMcpError = false
273
+ for (const toolName of SOLODIT_MCP_TOOLS) {
274
+ try {
275
+ const response = await mcpCaller(
276
+ SOLODIT_MCP_SERVER,
277
+ toolName,
278
+ buildMcpArgs(toolName, query, limit, args.severity),
279
+ )
177
280
 
178
- if (args.severity && args.severity.length > 0) {
179
- mcpArgs.filters = { severity: args.severity };
180
- }
281
+ if (hasMcpError(response)) {
282
+ hadMcpError = true
283
+ continue
284
+ }
181
285
 
182
- const response = await mcpCaller(SOLODIT_MCP_SERVER, SOLODIT_MCP_TOOL, mcpArgs);
183
- const findings = parseFindings(response);
286
+ const findings = filterFindingsBySeverity(
287
+ parseFindingsFromAnyResponse(response),
288
+ args.severity,
289
+ )
184
290
 
185
- return {
186
- results: findings,
187
- totalFound: findings.length,
188
- query,
189
- };
190
- } catch {
191
- // MCP bridge failed (upstream crash, connection error, etc.)
192
- // Fall through to HTTP fallback before giving up
193
- return callSoloditHttp(query, limit);
291
+ return {
292
+ results: findings.slice(0, limit),
293
+ totalFound: findings.length,
294
+ query,
295
+ }
296
+ } catch {
297
+ hadMcpError = true
298
+ }
299
+ }
300
+
301
+ const fallback = await callSoloditHttp(query, limit, args.severity, port)
302
+ if (fallback.error || hadMcpError) {
303
+ return fallback
194
304
  }
305
+
306
+ return fallback
307
+ }
308
+
309
+ export function createSoloditSearchTool(port: number = DEFAULT_SOLODIT_PORT): ToolDefinition {
310
+ return tool({
311
+ description:
312
+ "Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
313
+ args: {
314
+ query: tool.schema.string(),
315
+ severity: tool.schema.array(tool.schema.string()).optional(),
316
+ limit: tool.schema.number().optional(),
317
+ },
318
+ async execute(args, context) {
319
+ const result = await executeSoloditSearch(args, context, undefined, port)
320
+ return JSON.stringify(result)
321
+ },
322
+ })
195
323
  }
196
324
 
197
- export const soloditSearchTool = tool({
198
- description:
199
- "Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
200
- args: {
201
- query: tool.schema.string(),
202
- severity: tool.schema.array(tool.schema.string()).optional(),
203
- limit: tool.schema.number().optional(),
204
- },
205
- async execute(args, context) {
206
- const result = await executeSoloditSearch(args, context);
207
- return JSON.stringify(result);
208
- },
209
- });
325
+ export const soloditSearchTool = createSoloditSearchTool()
@@ -1,10 +1,11 @@
1
1
  import os from "node:os"
2
2
  import path from "node:path"
3
- import { tool, type ToolContext } from "@opencode-ai/plugin"
4
- import { ScvdClient } from "../knowledge/scvd-client"
5
- import { syncAll, syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
3
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
6
4
  import { loadArgusConfig } from "../config/loader"
7
5
  import type { ArgusConfig } from "../config/types"
6
+ import { ScvdClient } from "../knowledge/scvd-client"
7
+ import { type SyncResult, syncAll, syncIncremental } from "../knowledge/scvd-sync"
8
+ import { resolveProjectDir } from "../shared/project-utils"
8
9
 
9
10
  type SyncKnowledgeArgs = {
10
11
  force?: boolean
@@ -62,14 +63,14 @@ function toErrorMessage(error: unknown): string {
62
63
  export async function executeSyncKnowledge(
63
64
  args: SyncKnowledgeArgs,
64
65
  context: ToolContext,
65
- deps: SyncKnowledgeDependencies = {}
66
+ deps: SyncKnowledgeDependencies = {},
66
67
  ): Promise<SyncKnowledgeResult> {
67
68
  const dependencies = { ...defaultDependencies(), ...deps }
68
69
 
69
70
  context.metadata({ title: "Syncing SCVD knowledge index..." })
70
71
 
71
72
  try {
72
- const projectDir = context.directory ?? context.worktree ?? process.cwd()
73
+ const projectDir = resolveProjectDir(context)
73
74
  const argusConfig = dependencies.loadConfig(projectDir)
74
75
 
75
76
  if (!argusConfig.knowledge?.scvd?.enabled) {
@@ -81,12 +82,7 @@ export async function executeSyncKnowledge(
81
82
  }
82
83
 
83
84
  const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
84
- const indexPath = path.join(
85
- os.homedir(),
86
- ".cache",
87
- "solidity-argus",
88
- "scvd-index.json"
89
- )
85
+ const indexPath = path.join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
90
86
 
91
87
  const client = dependencies.createClient(apiUrl, context.abort)
92
88
  const result = args.force
@@ -1,10 +1,13 @@
1
- import { existsSync, readdirSync } from "fs";
2
- import { join } from "path";
1
+ import { existsSync, readdirSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { createLogger } from "../shared/logger"
4
+
5
+ const logger = createLogger()
3
6
 
4
7
  export interface AuditArtifact {
5
- type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output";
6
- path: string;
7
- name: string;
8
+ type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output"
9
+ path: string
10
+ name: string
8
11
  }
9
12
 
10
13
  /**
@@ -13,17 +16,17 @@ export interface AuditArtifact {
13
16
  * @returns Array of detected audit artifacts
14
17
  */
15
18
  export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
16
- const artifacts: AuditArtifact[] = [];
19
+ const artifacts: AuditArtifact[] = []
17
20
 
18
21
  if (!existsSync(projectDir)) {
19
- return artifacts;
22
+ return artifacts
20
23
  }
21
24
 
22
25
  try {
23
- const entries = readdirSync(projectDir, { withFileTypes: true });
26
+ const entries = readdirSync(projectDir, { withFileTypes: true })
24
27
 
25
28
  for (const entry of entries) {
26
- const fullPath = join(projectDir, entry.name);
29
+ const fullPath = join(projectDir, entry.name)
27
30
 
28
31
  // Check directories
29
32
  if (entry.isDirectory()) {
@@ -33,8 +36,8 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
33
36
  type: "audit-report",
34
37
  path: fullPath,
35
38
  name: entry.name,
36
- });
37
- continue;
39
+ })
40
+ continue
38
41
  }
39
42
 
40
43
  // Deployment artifact directories
@@ -43,28 +46,28 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
43
46
  type: "deployment-artifact",
44
47
  path: fullPath,
45
48
  name: entry.name,
46
- });
47
- continue;
49
+ })
50
+ continue
48
51
  }
49
52
 
50
53
  // docs/audit* directories
51
54
  if (entry.name === "docs") {
52
55
  try {
53
- const docsEntries = readdirSync(fullPath, { withFileTypes: true });
56
+ const docsEntries = readdirSync(fullPath, { withFileTypes: true })
54
57
  for (const docsEntry of docsEntries) {
55
58
  if (docsEntry.isDirectory() && docsEntry.name.startsWith("audit")) {
56
59
  artifacts.push({
57
60
  type: "audit-report",
58
61
  path: join(fullPath, docsEntry.name),
59
62
  name: docsEntry.name,
60
- });
63
+ })
61
64
  }
62
65
  }
63
66
  } catch {
64
- // Ignore errors reading docs directory
67
+ logger.debug("Failed to read docs directory for audit artifacts")
65
68
  }
66
69
  }
67
- continue;
70
+ continue
68
71
  }
69
72
 
70
73
  // Check files
@@ -78,8 +81,8 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
78
81
  type: "audit-report",
79
82
  path: fullPath,
80
83
  name: entry.name,
81
- });
82
- continue;
84
+ })
85
+ continue
83
86
  }
84
87
 
85
88
  // Slither output files
@@ -92,28 +95,24 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
92
95
  type: "slither-output",
93
96
  path: fullPath,
94
97
  name: entry.name,
95
- });
96
- continue;
98
+ })
99
+ continue
97
100
  }
98
101
 
99
102
  // Security tool output files
100
- if (
101
- /^mythril-report.*/.test(entry.name) ||
102
- /^securify-report.*/.test(entry.name)
103
- ) {
103
+ if (/^mythril-report.*/.test(entry.name) || /^securify-report.*/.test(entry.name)) {
104
104
  artifacts.push({
105
105
  type: "security-tool-output",
106
106
  path: fullPath,
107
107
  name: entry.name,
108
- });
109
- continue;
108
+ })
110
109
  }
111
110
  }
112
111
  }
113
112
  } catch {
114
113
  // Return empty array if directory cannot be read
115
- return [];
114
+ return []
116
115
  }
117
116
 
118
- return artifacts;
117
+ return artifacts
119
118
  }