solidity-argus 0.1.7 → 0.2.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.
Files changed (87) hide show
  1. package/README.md +161 -1
  2. package/package.json +5 -2
  3. package/skills/README.md +63 -0
  4. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  5. package/skills/manifests/cyfrin.json +16 -0
  6. package/skills/manifests/defifofum.json +25 -0
  7. package/skills/manifests/kadenzipfel.json +48 -0
  8. package/skills/manifests/scvd.json +9 -0
  9. package/skills/manifests/smartbugs.json +11 -0
  10. package/skills/manifests/solodit.json +9 -0
  11. package/skills/manifests/sunweb3sec.json +11 -0
  12. package/skills/manifests/trailofbits.json +9 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  14. package/skills/patterns/access-control.yaml +31 -0
  15. package/skills/patterns/erc4626.yaml +29 -0
  16. package/skills/patterns/flash-loan.yaml +20 -0
  17. package/skills/patterns/oracle.yaml +30 -0
  18. package/skills/patterns/proxy.yaml +30 -0
  19. package/skills/patterns/reentrancy.yaml +30 -0
  20. package/skills/patterns/signature.yaml +31 -0
  21. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  22. package/skills/references/exploit-reference/SKILL.md +3 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
  24. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
  25. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
  26. package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
  27. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
  28. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
  29. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
  30. package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
  31. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
  32. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
  33. package/src/agents/argus-prompt.ts +7 -7
  34. package/src/agents/pythia-prompt.ts +11 -11
  35. package/src/agents/scribe-prompt.ts +6 -6
  36. package/src/agents/sentinel-prompt.ts +7 -7
  37. package/src/cli/cli-output.ts +16 -0
  38. package/src/cli/cli-program.ts +9 -5
  39. package/src/cli/commands/doctor.ts +274 -16
  40. package/src/cli/commands/init.ts +5 -5
  41. package/src/cli/commands/install.ts +5 -5
  42. package/src/cli/commands/lint-skills.ts +114 -0
  43. package/src/cli/tui-prompts.ts +4 -2
  44. package/src/config/schema.ts +2 -0
  45. package/src/create-hooks.ts +141 -32
  46. package/src/create-tools.ts +2 -0
  47. package/src/features/error-recovery/session-recovery.ts +7 -1
  48. package/src/features/error-recovery/tool-error-recovery.ts +74 -19
  49. package/src/features/persistent-state/audit-state-manager.ts +36 -13
  50. package/src/hooks/agent-tracker.ts +53 -0
  51. package/src/hooks/compaction-hook.ts +46 -37
  52. package/src/hooks/config-handler.ts +22 -9
  53. package/src/hooks/context-budget.ts +45 -0
  54. package/src/hooks/event-hook-v2.ts +8 -2
  55. package/src/hooks/event-hook.ts +5 -4
  56. package/src/hooks/knowledge-sync-hook.ts +2 -1
  57. package/src/hooks/recon-context-builder.ts +66 -0
  58. package/src/hooks/safe-create-hook.ts +4 -5
  59. package/src/hooks/system-prompt-hook.ts +92 -221
  60. package/src/hooks/tool-tracking-hook.ts +108 -9
  61. package/src/hooks/types.ts +0 -1
  62. package/src/index.ts +28 -6
  63. package/src/knowledge/retry.ts +53 -0
  64. package/src/knowledge/scvd-client.ts +37 -10
  65. package/src/knowledge/scvd-errors.ts +89 -0
  66. package/src/knowledge/scvd-index.ts +53 -3
  67. package/src/knowledge/scvd-sync.ts +205 -34
  68. package/src/knowledge/source-manifest.ts +102 -0
  69. package/src/plugin-interface.ts +11 -3
  70. package/src/shared/binary-utils.ts +1 -0
  71. package/src/shared/logger.ts +78 -17
  72. package/src/skills/argus-skill-resolver.ts +226 -0
  73. package/src/skills/skill-schema.ts +98 -0
  74. package/src/state/audit-state.ts +2 -0
  75. package/src/state/types.ts +32 -1
  76. package/src/tools/argus-skill-load-tool.ts +73 -0
  77. package/src/tools/pattern-checker-tool.ts +56 -12
  78. package/src/tools/pattern-loader.ts +183 -0
  79. package/src/tools/pattern-schema.ts +51 -0
  80. package/src/tools/report-generator-tool.ts +134 -11
  81. package/src/tools/slither-tool.ts +61 -19
  82. package/src/tools/solodit-search-tool.ts +92 -14
  83. package/src/utils/audit-artifact-detector.ts +119 -0
  84. package/src/utils/dependency-scanner.ts +93 -0
  85. package/src/utils/project-detector.ts +128 -26
  86. package/src/utils/solidity-parser.ts +20 -4
  87. package/src/utils/solodit-health.ts +29 -0
@@ -0,0 +1,183 @@
1
+ import { readdirSync, readFileSync, existsSync } from "node:fs"
2
+ import { join, extname } from "node:path"
3
+ import { parse as parseYaml } from "yaml"
4
+ import { PatternPackSchema, type PatternDefinition } from "./pattern-schema"
5
+ import { createLogger } from "../shared/logger"
6
+ import { parseFrontmatter, SkillFrontmatterSchema } from "../skills/skill-schema"
7
+
8
+ const logger = createLogger()
9
+
10
+ const YAML_EXTENSIONS = new Set([".yaml", ".yml"])
11
+
12
+ const SKILL_NAME_TO_PATTERN_CATEGORY: Record<string, PatternDefinition["category"]> = {
13
+ "reentrancy": "reentrancy",
14
+ "access-control": "access-control",
15
+ "oracle-manipulation": "oracle-manipulation",
16
+ "flash-loan-attacks": "flash-loan",
17
+ "delegatecall-untrusted-callee": "proxy",
18
+ "authorization-txorigin": "access-control",
19
+ "unchecked-return-values": "logic-error",
20
+ "dos-revert": "dos",
21
+ "overflow-underflow": "logic-error",
22
+ "signature-malleability": "signature",
23
+ }
24
+
25
+ export function loadPatternPacks(patternsDir: string): PatternDefinition[] {
26
+ if (!existsSync(patternsDir)) {
27
+ logger.warn(`Patterns directory does not exist: ${patternsDir}`)
28
+ return []
29
+ }
30
+
31
+ const entries = readdirSync(patternsDir).filter((f) =>
32
+ YAML_EXTENSIONS.has(extname(f).toLowerCase())
33
+ )
34
+
35
+ const allPatterns: PatternDefinition[] = []
36
+
37
+ for (const filename of entries) {
38
+ const filePath = join(patternsDir, filename)
39
+ try {
40
+ const raw = readFileSync(filePath, "utf-8")
41
+ const parsed = parseYaml(raw)
42
+ const result = PatternPackSchema.safeParse(parsed)
43
+
44
+ if (!result.success) {
45
+ logger.warn(
46
+ `Skipping ${filename}: schema validation failed — ${result.error.issues[0]?.message ?? "unknown"}`
47
+ )
48
+ continue
49
+ }
50
+
51
+ allPatterns.push(...result.data.patterns)
52
+ } catch (err) {
53
+ logger.warn(
54
+ `Skipping ${filename}: ${err instanceof Error ? err.message : "parse error"}`
55
+ )
56
+ }
57
+ }
58
+
59
+ return allPatterns
60
+ }
61
+
62
+ function listSkillMarkdownFiles(skillsDir: string): string[] {
63
+ if (!existsSync(skillsDir)) {
64
+ logger.warn(`Skills directory does not exist: ${skillsDir}`)
65
+ return []
66
+ }
67
+
68
+ const files: string[] = []
69
+ const stack = [skillsDir]
70
+
71
+ while (stack.length > 0) {
72
+ const current = stack.pop()
73
+ if (!current) continue
74
+
75
+ const entries = readdirSync(current, { withFileTypes: true })
76
+ for (const entry of entries) {
77
+ const fullPath = join(current, entry.name)
78
+ if (entry.isDirectory()) {
79
+ stack.push(fullPath)
80
+ continue
81
+ }
82
+
83
+ if (entry.isFile() && entry.name === "SKILL.md") {
84
+ files.push(fullPath)
85
+ }
86
+ }
87
+ }
88
+
89
+ return files
90
+ }
91
+
92
+ export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefinition[] {
93
+ const skillFiles = listSkillMarkdownFiles(skillsDir)
94
+ const extracted: PatternDefinition[] = []
95
+
96
+ for (const filePath of skillFiles) {
97
+ try {
98
+ const content = readFileSync(filePath, "utf-8")
99
+ const frontmatter = parseFrontmatter(content)
100
+ if (!frontmatter) continue
101
+
102
+ const parsed = SkillFrontmatterSchema.safeParse(frontmatter)
103
+ if (!parsed.success) continue
104
+
105
+ const skillName = parsed.data.name
106
+ const category = SKILL_NAME_TO_PATTERN_CATEGORY[skillName]
107
+ if (!category) continue
108
+
109
+ const rules = parsed.data.detection_rules
110
+ if (!rules || rules.length === 0) continue
111
+
112
+ for (const [index, rule] of rules.entries()) {
113
+ extracted.push({
114
+ name: `${skillName}-rule-${index + 1}`,
115
+ category,
116
+ severity: rule.severity,
117
+ confidence: rule.confidence ?? "Medium",
118
+ version: "1.0",
119
+ regex: rule.regex,
120
+ description: rule.description ?? `Detection rule from ${skillName} SKILL.md`,
121
+ ...(rule.swc ? { swc: rule.swc } : {}),
122
+ })
123
+ }
124
+ } catch (err) {
125
+ logger.warn(
126
+ `Skipping ${filePath}: ${err instanceof Error ? err.message : "parse error"}`
127
+ )
128
+ }
129
+ }
130
+
131
+ return extracted
132
+ }
133
+
134
+ type BuiltinPattern = {
135
+ name: string
136
+ category: string
137
+ severity: string
138
+ regex: RegExp
139
+ description: string
140
+ exploitReference?: string
141
+ }
142
+
143
+ function isValidUrl(s: string): boolean {
144
+ try {
145
+ new URL(s)
146
+ return true
147
+ } catch {
148
+ return false
149
+ }
150
+ }
151
+
152
+ function builtinToDefinition(b: BuiltinPattern): PatternDefinition {
153
+ return {
154
+ name: b.name,
155
+ category: b.category as PatternDefinition["category"],
156
+ severity: b.severity as PatternDefinition["severity"],
157
+ confidence: "Medium",
158
+ version: "1.0",
159
+ regex: b.regex.source,
160
+ description: b.description,
161
+ ...(b.exploitReference && isValidUrl(b.exploitReference)
162
+ ? { exploit_ref: b.exploitReference }
163
+ : {}),
164
+ }
165
+ }
166
+
167
+ export function mergeWithBuiltins(
168
+ yamlPatterns: PatternDefinition[],
169
+ builtins: BuiltinPattern[],
170
+ skillDetectionRules: PatternDefinition[] = []
171
+ ): PatternDefinition[] {
172
+ const mergedInputs = [...yamlPatterns, ...skillDetectionRules]
173
+ const yamlByName = new Map(mergedInputs.map((p) => [p.name, p]))
174
+ const merged: PatternDefinition[] = [...mergedInputs]
175
+
176
+ for (const builtin of builtins) {
177
+ if (!yamlByName.has(builtin.name)) {
178
+ merged.push(builtinToDefinition(builtin))
179
+ }
180
+ }
181
+
182
+ return merged
183
+ }
@@ -0,0 +1,51 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * Canonical pattern category taxonomy.
5
+ * Every builtin, YAML, and skill-derived pattern must belong to one of these.
6
+ */
7
+ export const PATTERN_CATEGORIES = [
8
+ "reentrancy",
9
+ "oracle-manipulation",
10
+ "flash-loan",
11
+ "access-control",
12
+ "erc4626",
13
+ "proxy",
14
+ "signature",
15
+ "dos",
16
+ "front-running",
17
+ "governance",
18
+ "token-standard",
19
+ "gas-optimization",
20
+ "logic-error",
21
+ "delegatecall",
22
+ ] as const
23
+
24
+ export const PatternCategorySchema = z.enum(PATTERN_CATEGORIES)
25
+
26
+ export const PatternDefinitionSchema = z.object({
27
+ name: z.string().min(1).max(128),
28
+ category: PatternCategorySchema,
29
+ severity: z.enum(["Critical", "High", "Medium", "Low", "Informational"]),
30
+ swc: z
31
+ .string()
32
+ .regex(/^SWC-\d+$/)
33
+ .optional(),
34
+ confidence: z.enum(["High", "Medium", "Low"]).default("Medium"),
35
+ version: z.string().default("1.0"),
36
+ regex: z.string().min(1),
37
+ description: z.string().min(1),
38
+ exploit_ref: z.string().url().optional(),
39
+ remediation: z.string().optional(),
40
+ })
41
+
42
+ export type PatternDefinition = z.infer<typeof PatternDefinitionSchema>
43
+ export type PatternCategory = z.infer<typeof PatternCategorySchema>
44
+
45
+ export const PatternPackSchema = z.object({
46
+ pack_name: z.string().optional(),
47
+ pack_version: z.string().default("1.0"),
48
+ patterns: z.array(PatternDefinitionSchema).min(1),
49
+ })
50
+
51
+ export type PatternPack = z.infer<typeof PatternPackSchema>
@@ -1,5 +1,5 @@
1
1
  import { tool, type ToolContext } from "@opencode-ai/plugin";
2
- import type { AuditState, Finding, FindingSeverity } from "../state/types";
2
+ import type { AuditState, Finding, FindingSeverity, ToolExecution } from "../state/types";
3
3
 
4
4
  type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational";
5
5
 
@@ -67,7 +67,20 @@ function emptyCounts(): FindingsCount {
67
67
  };
68
68
  }
69
69
 
70
- function parseAuditState(auditState: string): Finding[] {
70
+ function emptyAuditState(findings: Finding[] = []): AuditState {
71
+ return {
72
+ sessionId: "",
73
+ projectDir: "",
74
+ contractsReviewed: [],
75
+ findings,
76
+ toolsExecuted: [],
77
+ currentPhase: "complete",
78
+ scope: [],
79
+ startTime: 0,
80
+ };
81
+ }
82
+
83
+ export function parseAuditState(auditState: string): AuditState {
71
84
  let parsed: unknown;
72
85
  try {
73
86
  parsed = JSON.parse(auditState);
@@ -76,14 +89,18 @@ function parseAuditState(auditState: string): Finding[] {
76
89
  }
77
90
 
78
91
  if (Array.isArray(parsed)) {
79
- return parsed as Finding[];
92
+ return emptyAuditState(parsed as Finding[]);
80
93
  }
81
94
 
82
95
  if (typeof parsed === "object" && parsed !== null && Array.isArray((parsed as AuditState).findings)) {
83
- return (parsed as AuditState).findings;
96
+ const state = parsed as AuditState;
97
+ return {
98
+ ...emptyAuditState(),
99
+ ...state,
100
+ };
84
101
  }
85
102
 
86
- return [];
103
+ return emptyAuditState();
87
104
  }
88
105
 
89
106
  function normalizeTitle(check: string): string {
@@ -215,13 +232,123 @@ function buildFindingsSection(findings: Finding[]): string {
215
232
  return lines.join("\n");
216
233
  }
217
234
 
235
+ function formatDuration(ms: number): string {
236
+ if (ms < 1000) return `${ms}ms`;
237
+ return `${(ms / 1000).toFixed(1)}s`;
238
+ }
239
+
240
+ export function buildProvenanceAppendix(
241
+ state: AuditState,
242
+ threshold: SeverityThreshold,
243
+ includedCount: number,
244
+ ): string {
245
+ const lines: string[] = ["## Appendix: Data Provenance"];
246
+
247
+ lines.push("- Data source: `audit_state` payload");
248
+ lines.push(`- Severity threshold applied: ${threshold}`);
249
+ lines.push(`- Findings included in report: ${includedCount}`);
250
+
251
+ if (state.findings.length > 0) {
252
+ const sourceCounts: Record<string, number> = {};
253
+ for (const f of state.findings) {
254
+ sourceCounts[f.source] = (sourceCounts[f.source] ?? 0) + 1;
255
+ }
256
+ lines.push("");
257
+ lines.push("### Source Breakdown");
258
+ lines.push("");
259
+ lines.push("| Source | Count |");
260
+ lines.push("| --- | ---: |");
261
+ for (const [source, count] of Object.entries(sourceCounts).sort(
262
+ (a, b) => b[1] - a[1],
263
+ )) {
264
+ lines.push(`| ${source} | ${count} |`);
265
+ }
266
+ }
267
+
268
+ if (state.toolsExecuted.length > 0) {
269
+ lines.push("");
270
+ lines.push("### Tool Execution Summary");
271
+ lines.push("");
272
+ lines.push("| Tool | Duration | Status | Findings |");
273
+ lines.push("| --- | --- | --- | ---: |");
274
+ for (const exec of state.toolsExecuted) {
275
+ const duration =
276
+ exec.endTime != null
277
+ ? formatDuration(exec.endTime - exec.startTime)
278
+ : "—";
279
+ const status = exec.success ? "✅ success" : "❌ failure";
280
+ lines.push(
281
+ `| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`,
282
+ );
283
+ }
284
+ }
285
+
286
+ const syncExec = state.toolsExecuted.find((t) => t.tool === "argus_sync_knowledge");
287
+ if (state.patternVersion || syncExec) {
288
+ lines.push("");
289
+ lines.push("### Data Freshness");
290
+ lines.push("");
291
+ if (state.patternVersion) {
292
+ lines.push(`- Pattern pack version: \`${state.patternVersion}\``);
293
+ }
294
+ if (syncExec) {
295
+ lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`);
296
+ }
297
+ }
298
+
299
+ if (state.soloditResults && state.soloditResults.length > 0) {
300
+ lines.push("");
301
+ lines.push("### Solodit Cross-References");
302
+ lines.push("");
303
+ for (const result of state.soloditResults) {
304
+ lines.push(`**Query**: "${result.query}" — ${result.resultCount} results`);
305
+ if (result.topResults.length > 0) {
306
+ lines.push("");
307
+ lines.push("| Title | Severity | Protocol |");
308
+ lines.push("| --- | --- | --- |");
309
+ for (const top of result.topResults) {
310
+ lines.push(`| ${top.title} | ${top.severity} | ${top.protocol} |`);
311
+ }
312
+ }
313
+ lines.push("");
314
+ }
315
+ }
316
+
317
+ if (state.fuzzCounterexamples && state.fuzzCounterexamples.length > 0) {
318
+ lines.push("");
319
+ lines.push("### Fuzz Evidence");
320
+ lines.push("");
321
+ lines.push("| Test | Inputs | Runs | Revert Reason |");
322
+ lines.push("| --- | --- | ---: | --- |");
323
+ for (const cx of state.fuzzCounterexamples) {
324
+ const inputs = cx.inputs.join(", ");
325
+ const reason = cx.revertReason ?? "—";
326
+ lines.push(`| ${cx.testName} | ${inputs} | ${cx.runs} | ${reason} |`);
327
+ }
328
+ }
329
+
330
+ if (state.skillsLoaded && state.skillsLoaded.length > 0) {
331
+ lines.push("");
332
+ lines.push("### Knowledge Sources");
333
+ lines.push("");
334
+ lines.push("Skills loaded during this audit:");
335
+ lines.push("");
336
+ for (const skill of state.skillsLoaded) {
337
+ lines.push(`- ${skill}`);
338
+ }
339
+ }
340
+
341
+ return lines.join("\n");
342
+ }
343
+
218
344
  export async function executeReportGeneration(
219
345
  args: ReportGeneratorArgs,
220
346
  context: ToolContext
221
347
  ): Promise<ReportGenerationResult> {
222
348
  const includeExecutiveSummary = args.include_executive_summary ?? true;
223
349
  const threshold = args.severity_threshold ?? "low";
224
- const findings = parseAuditState(args.audit_state).filter((finding) =>
350
+ const state = parseAuditState(args.audit_state);
351
+ const findings = state.findings.filter((finding) =>
225
352
  shouldIncludeFinding(finding, threshold)
226
353
  );
227
354
  const counts = calculateCounts(findings);
@@ -276,11 +403,7 @@ export async function executeReportGeneration(
276
403
  sections.push(`- ${item}`);
277
404
  }
278
405
 
279
- sections.push("## Appendix");
280
- sections.push("Tool execution summary:");
281
- sections.push("- Data source: `audit_state` payload");
282
- sections.push(`- Severity threshold applied: ${threshold}`);
283
- sections.push(`- Findings included in report: ${findings.length}`);
406
+ sections.push(buildProvenanceAppendix(state, threshold, findings.length));
284
407
 
285
408
  return {
286
409
  report: sections.join("\n\n"),
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "crypto";
2
2
  import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
3
- import { join } from "node:path";
3
+ import { join, resolve, dirname, isAbsolute } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { execSync } from "node:child_process";
6
6
  import { tool, type ToolContext } from "@opencode-ai/plugin";
@@ -44,7 +44,8 @@ export type SlitherRunResult = {
44
44
 
45
45
  export type RunSlitherCommand = (
46
46
  command: string[],
47
- signal: AbortSignal
47
+ signal: AbortSignal,
48
+ cwd: string
48
49
  ) => Promise<SlitherRunResult>;
49
50
 
50
51
  export type SlitherAnalyzeResult = {
@@ -167,8 +168,9 @@ function ensureSolc(version: string): boolean {
167
168
  }
168
169
  }
169
170
 
170
- export const runSlitherCommand: RunSlitherCommand = async (command, signal) => {
171
+ export const runSlitherCommand: RunSlitherCommand = async (command, signal, cwd) => {
171
172
  const child = Bun.spawn(command, {
173
+ cwd,
172
174
  stdout: "pipe",
173
175
  stderr: "pipe",
174
176
  signal,
@@ -194,6 +196,7 @@ export type FlattenFallbackDeps = {
194
196
  parseSolcVersion: (target: string) => string | undefined;
195
197
  extractContractNames: (filePath: string) => string[];
196
198
  execSyncFn: typeof execSync;
199
+ cwd: string;
197
200
  };
198
201
 
199
202
  const defaultFlattenDeps: FlattenFallbackDeps = {
@@ -203,6 +206,7 @@ const defaultFlattenDeps: FlattenFallbackDeps = {
203
206
  parseSolcVersion,
204
207
  extractContractNames,
205
208
  execSyncFn: execSync,
209
+ cwd: process.cwd(),
206
210
  };
207
211
 
208
212
  export async function flattenFallback(
@@ -213,12 +217,26 @@ export async function flattenFallback(
213
217
  const startedAt = Date.now();
214
218
 
215
219
  if (!deps.hasBinary("forge")) {
216
- return undefined;
220
+ return {
221
+ success: false,
222
+ findingsCount: 0,
223
+ findings: [],
224
+ executionTime: Date.now() - startedAt,
225
+ errors: ["forge binary not found — required for via_ir flatten fallback"],
226
+ error: "forge binary not found — required for via_ir flatten fallback",
227
+ };
217
228
  }
218
229
 
219
230
  const solcVersion = args.solc_version ?? deps.parseSolcVersion(args.target);
220
231
  if (!solcVersion) {
221
- return undefined;
232
+ return {
233
+ success: false,
234
+ findingsCount: 0,
235
+ findings: [],
236
+ executionTime: Date.now() - startedAt,
237
+ errors: ["Could not determine solc version from foundry.toml or pragma — required for flatten fallback"],
238
+ error: "Could not determine solc version from foundry.toml or pragma — required for flatten fallback",
239
+ };
222
240
  }
223
241
 
224
242
  if (!deps.ensureSolc(solcVersion)) {
@@ -241,6 +259,7 @@ export async function flattenFallback(
241
259
  solFiles = deps.execSyncFn(`find "${srcDir}" -name "*.sol" -maxdepth 3 -not -path "*/mocks/*" -not -path "*/test/*"`, {
242
260
  encoding: "utf-8",
243
261
  timeout: 5_000,
262
+ stdio: ["pipe", "pipe", "pipe"],
244
263
  })
245
264
  .trim()
246
265
  .split("\n")
@@ -268,7 +287,8 @@ export async function flattenFallback(
268
287
  const flattened = deps.execSyncFn(`forge flatten "${solFile}"`, {
269
288
  encoding: "utf-8",
270
289
  timeout: 30_000,
271
- cwd: args.target.endsWith(".sol") ? undefined : args.target,
290
+ cwd: deps.cwd,
291
+ stdio: ["pipe", "pipe", "pipe"],
272
292
  });
273
293
  writeFileSync(flatFile, flattened);
274
294
  } catch (_e) {
@@ -286,7 +306,7 @@ export async function flattenFallback(
286
306
  ];
287
307
 
288
308
  try {
289
- const runResult = await deps.runCommand(command, context.abort);
309
+ const runResult = await deps.runCommand(command, context.abort, deps.cwd);
290
310
 
291
311
  let payload: SlitherPayload;
292
312
  try {
@@ -358,8 +378,10 @@ function parseFindings(payload: SlitherPayload): Finding[] {
358
378
  export async function executeSlitherAnalyze(
359
379
  args: SlitherArgs,
360
380
  context: ToolContext,
361
- runCommand: RunSlitherCommand = runSlitherCommand
381
+ runCommand: RunSlitherCommand = runSlitherCommand,
382
+ cwd?: string
362
383
  ): Promise<SlitherAnalyzeResult> {
384
+ const projectDir = cwd ?? context.directory ?? context.worktree ?? process.cwd();
363
385
  const startedAt = Date.now();
364
386
  context.metadata({ title: `Slither analysis: ${args.target}` });
365
387
 
@@ -367,6 +389,7 @@ export async function executeSlitherAnalyze(
367
389
  const fallbackResult = await flattenFallback(args, context, {
368
390
  ...defaultFlattenDeps,
369
391
  runCommand,
392
+ cwd: projectDir,
370
393
  });
371
394
  if (fallbackResult) return fallbackResult;
372
395
  return {
@@ -382,7 +405,7 @@ export async function executeSlitherAnalyze(
382
405
  const command = buildCommand(args);
383
406
 
384
407
  try {
385
- const runResult = await runCommand(command, context.abort);
408
+ const runResult = await runCommand(command, context.abort, projectDir);
386
409
  const errors: string[] = [];
387
410
 
388
411
  if (runResult.exitCode !== 0) {
@@ -401,6 +424,7 @@ export async function executeSlitherAnalyze(
401
424
  const fallbackResult = await flattenFallback(args, context, {
402
425
  ...defaultFlattenDeps,
403
426
  runCommand,
427
+ cwd: projectDir,
404
428
  });
405
429
  if (fallbackResult) return fallbackResult;
406
430
  }
@@ -425,6 +449,7 @@ export async function executeSlitherAnalyze(
425
449
  const fallbackResult = await flattenFallback(args, context, {
426
450
  ...defaultFlattenDeps,
427
451
  runCommand,
452
+ cwd: projectDir,
428
453
  });
429
454
  if (fallbackResult) return fallbackResult;
430
455
  }
@@ -474,15 +499,24 @@ export async function executeSlitherAnalyze(
474
499
  }
475
500
 
476
501
  export function detectViaIr(target: string): boolean {
477
- const projectDir = target.endsWith(".sol") ? join(target, "..") : target;
478
- const foundryTomlPath = join(projectDir, "foundry.toml");
479
- if (!existsSync(foundryTomlPath)) return false;
480
- try {
481
- const content = readFileSync(foundryTomlPath, "utf-8");
482
- return /^\s*via[_-]ir\s*=\s*true/m.test(content);
483
- } catch {
484
- return false;
502
+ let dir = resolve(target.endsWith(".sol") ? dirname(target) : target);
503
+ const root = resolve("/");
504
+
505
+ while (true) {
506
+ const foundryTomlPath = join(dir, "foundry.toml");
507
+ if (existsSync(foundryTomlPath)) {
508
+ try {
509
+ const content = readFileSync(foundryTomlPath, "utf-8");
510
+ if (/^\s*via[_-]ir\s*=\s*true/m.test(content)) return true;
511
+ } catch {
512
+ // unreadable file — keep walking
513
+ }
514
+ }
515
+ if (dir === root) break;
516
+ dir = dirname(dir);
485
517
  }
518
+
519
+ return false;
486
520
  }
487
521
 
488
522
  export const slitherTool = tool({
@@ -493,10 +527,18 @@ export const slitherTool = tool({
493
527
  detectors: tool.schema.array(tool.schema.string()).optional(),
494
528
  exclude: tool.schema.array(tool.schema.string()).optional(),
495
529
  solc_version: tool.schema.string().optional(),
530
+ via_ir: tool.schema.boolean().optional(),
496
531
  },
497
532
  async execute(args, context) {
498
- const viaIr = detectViaIr(args.target);
499
- const result = await executeSlitherAnalyze({ ...args, via_ir: viaIr }, context);
533
+ const projectDir = context.directory ?? context.worktree ?? process.cwd();
534
+ const resolvedTarget = isAbsolute(args.target) ? args.target : resolve(projectDir, args.target);
535
+ const viaIr = args.via_ir ?? detectViaIr(resolvedTarget);
536
+ const result = await executeSlitherAnalyze(
537
+ { ...args, target: resolvedTarget, via_ir: viaIr },
538
+ context,
539
+ runSlitherCommand,
540
+ projectDir,
541
+ );
500
542
  return JSON.stringify(result);
501
543
  },
502
544
  });