solidity-argus 0.5.8 → 0.5.9

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/AGENTS.md CHANGED
@@ -19,21 +19,21 @@ CLI: `argus doctor`, `argus init`, `argus install`.
19
19
 
20
20
  **Role**: Static analysis and testing specialist
21
21
  **Description**: Finds vulnerabilities through Slither static analysis, Foundry testing, fuzzing, and pattern matching. The tactical executor — runs tools, writes PoC tests, and verifies findings. Dispatched by Argus during Automated Scanning and Testing & Verification phases.
22
- **Model**: anthropic/claude-sonnet-4-7
22
+ **Model**: anthropic/claude-sonnet-4-6
23
23
  **Tools**: argus_slither_analyze, argus_forge_test, argus_gas_analysis, argus_forge_fuzz, argus_forge_coverage, argus_analyze_contract, argus_check_patterns, argus_proxy_detection, argus_record_finding, skill
24
24
 
25
25
  ## pythia
26
26
 
27
27
  **Role**: Vulnerability researcher
28
28
  **Description**: Consults Solodit, SCVD, and the knowledge base to find historical precedents and known attack vectors. Searches 7,769+ real-world audit findings and 51 curated vulnerability pattern files. Dispatched by Argus during Vulnerability Research phase.
29
- **Model**: anthropic/claude-sonnet-4-7
29
+ **Model**: anthropic/claude-sonnet-4-6
30
30
  **Tools**: argus_solodit_search, argus_check_patterns, argus_record_finding, skill
31
31
 
32
32
  ## scribe
33
33
 
34
34
  **Role**: Audit report writer
35
35
  **Description**: Transforms raw findings into professional markdown audit reports. Produces structured output with severity classifications (Critical/High/Medium/Low/Informational), impact assessments, proof-of-concept steps, and actionable recommendations. Dispatched by Argus only after all analysis is complete.
36
- **Model**: anthropic/claude-sonnet-4-7
36
+ **Model**: anthropic/claude-sonnet-4-6
37
37
  **Tools**: argus_read_findings, argus_persist_deduped, argus_generate_report, skill
38
38
 
39
39
  ## themis
package/README.md CHANGED
@@ -66,9 +66,9 @@ Argus will automatically:
66
66
  | Agent | Role | Model |
67
67
  |-------|------|-------|
68
68
  | `@argus` | Orchestrator — coordinates the full audit | claude-opus-4-7 |
69
- | `@sentinel` | Static analysis & testing specialist | claude-sonnet-4-7 |
70
- | `@pythia` | Vulnerability researcher | claude-sonnet-4-7 |
71
- | `@scribe` | Audit report writer | claude-sonnet-4-7 |
69
+ | `@sentinel` | Static analysis & testing specialist | claude-sonnet-4-6 |
70
+ | `@pythia` | Vulnerability researcher | claude-sonnet-4-6 |
71
+ | `@scribe` | Audit report writer | claude-sonnet-4-6 |
72
72
  | `@themis` | Independent audit quality gate | gpt-5.5 |
73
73
 
74
74
  ### @argus — The Orchestrator
@@ -285,9 +285,9 @@ Create `.argus/solidity-argus.jsonc` in your project root. `.opencode/solidity-a
285
285
  {
286
286
  "agents": {
287
287
  "argus": { "model": "anthropic/claude-opus-4-7" },
288
- "sentinel": { "model": "anthropic/claude-sonnet-4-7" },
289
- "pythia": { "model": "anthropic/claude-sonnet-4-7" },
290
- "scribe": { "model": "anthropic/claude-sonnet-4-7" },
288
+ "sentinel": { "model": "anthropic/claude-sonnet-4-6" },
289
+ "pythia": { "model": "anthropic/claude-sonnet-4-6" },
290
+ "scribe": { "model": "anthropic/claude-sonnet-4-6" },
291
291
  "themis": { "model": "openai/gpt-5.5" }
292
292
  },
293
293
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidity-argus",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
4
4
  "description": "Solidity smart contract security auditing plugin for OpenCode — 5 specialized agents, 15 tools (14 core + optional Solodit), and a curated vulnerability knowledge base",
5
5
  "keywords": [
6
6
  "solidity",
@@ -527,7 +527,7 @@ Scope: {list of audited files}
527
527
 
528
528
  STEPS:
529
529
  1. Call argus_read_findings with run_id above to load all findings
530
- 2. Deduplicate: group findings by vulnerability class + code location, merge into single entries
530
+ 2. Deduplicate: group findings by vulnerability class + code location, merge into single entries. Include \`observation_ids\` on every deduped finding so each raw finding maps to exactly one report entry.
531
531
  3. Enrich: for each Critical/High finding, write specific impact and recommendation
532
532
  4. Call argus_persist_deduped with run_id and your deduped findings array — this writes the source-of-truth JSON to disk
533
533
  5. Call argus_generate_report with run_id, project_name, and scope — the tool reads deduped findings from disk
@@ -538,7 +538,7 @@ Overall risk assessment: {your assessment}
538
538
 
539
539
  Scribe will:
540
540
  1. Read raw findings (may contain duplicates from different tools)
541
- 2. Semantically deduplicate (e.g., merge reentrancy-eth + reentrancy-cei-violation at same location)
541
+ 2. Semantically deduplicate (e.g., merge reentrancy-eth + reentrancy-cei-violation at same location) while preserving \`observation_ids\` lineage for every raw finding
542
542
  3. Enrich Critical/High findings with specific impact and recommendation text
543
543
  4. Persist deduped findings to disk via \`argus_persist_deduped\` (source-of-truth JSON)
544
544
  5. Call \`argus_generate_report\` with \`run_id\` — the tool reads from disk and renders markdown
@@ -53,6 +53,7 @@ Argus provides you with a \`run_id\`. Your job: read findings, deduplicate, enri
53
53
  - Add "**Detected by:**" listing all tools/checks that flagged it
54
54
  - Example: reentrancy-eth + reentrancy-cei-violation + reentrancy-eth-withdraw-state-after-call at VulnerableVault.sol:18-23 → ONE finding
55
55
  - **PRESERVATION RULE**: Every raw finding MUST map to exactly one deduped finding. Only merge findings that are genuinely the SAME vulnerability at the SAME location. Different vulnerability classes (e.g., default-visibility vs dos-revert) are SEPARATE findings even if both are Informational. NEVER drop findings during deduplication.
56
+ - **LINEAGE RULE**: Every deduped finding MUST include \`observation_ids\` containing each raw finding's \`observation_id\`, plus \`observation_count\`, \`sources\`, and \`reported_by_agents\` when available. This lets \`argus_generate_report\` prove raw-to-deduped parity instead of emitting a "Finding parity not verifiable" warning.
56
57
 
57
58
  3. **Enrich** (MANDATORY for Critical/High):
58
59
  - Write specific \`impact\` (concrete consequence, not "could be exploited")
@@ -61,7 +62,7 @@ Argus provides you with a \`run_id\`. Your job: read findings, deduplicate, enri
61
62
 
62
63
  4. **Persist deduped findings**: Call \`argus_persist_deduped\` with:
63
64
  - \`run_id\`: the run ID from Argus
64
- - \`deduped_findings\`: JSON array of your deduped and enriched findings
65
+ - \`deduped_findings\`: JSON array of your deduped and enriched findings, including \`observation_ids\` lineage for every merged raw observation
65
66
 
66
67
  This writes the source-of-truth JSON to disk at \`.argus/runs/{run_id}/deduped-findings.json\`.
67
68
 
@@ -13,6 +13,8 @@ import {
13
13
  } from "../../skills/argus-skill-resolver"
14
14
  import { parseFrontmatter, validateSkillFrontmatter } from "../../skills/skill-schema"
15
15
  import { detectViaIr } from "../../tools/slither-tool"
16
+ import { DEFAULT_SOLODIT_PORT } from "../../tools/solodit-search-tool"
17
+ import { checkSoloditHealth } from "../../utils/solodit-health"
16
18
  import { cliOutput } from "../cli-output"
17
19
  import type { CliCommand } from "../types"
18
20
 
@@ -459,21 +461,13 @@ export const doctorCommand: CliCommand = {
459
461
 
460
462
  const soloditEnabled = config?.solodit?.enabled !== false
461
463
  if (soloditEnabled) {
462
- try {
463
- const response = await fetch(
464
- "https://solodit.cyfrin.io/api/trpc/findings.get?batch=1&input=" +
465
- encodeURIComponent(JSON.stringify({ 0: "[]" })),
466
- {
467
- signal: AbortSignal.timeout(5000),
468
- },
469
- )
470
- if (response.ok) {
471
- cliOutput.log(`${GREEN}✓${RESET} Solodit API: reachable`)
472
- } else {
473
- cliOutput.log(`${YELLOW}⚠${RESET} Solodit API: returned ${response.status}`)
474
- }
475
- } catch {
476
- cliOutput.log(`${YELLOW}⚠${RESET} Solodit API: unreachable`)
464
+ const port = config?.solodit?.port ?? DEFAULT_SOLODIT_PORT
465
+ const status = await checkSoloditHealth(port, true)
466
+ if (status.reachable) {
467
+ cliOutput.log(`${GREEN}✓${RESET} Solodit MCP: reachable on port ${port}`)
468
+ } else {
469
+ const suffix = status.error ? ` (${status.error})` : ""
470
+ cliOutput.log(`${YELLOW}⚠${RESET} Solodit MCP: unreachable on port ${port}${suffix}`)
477
471
  }
478
472
  } else {
479
473
  cliOutput.log(`${YELLOW}⚠${RESET} Solodit: disabled in config`)
@@ -1,8 +1,8 @@
1
1
  export const DEFAULT_MODELS = {
2
2
  argus: "anthropic/claude-opus-4-7",
3
- sentinel: "anthropic/claude-sonnet-4-7",
4
- pythia: "anthropic/claude-sonnet-4-7",
5
- scribe: "anthropic/claude-sonnet-4-7",
3
+ sentinel: "anthropic/claude-sonnet-4-6",
4
+ pythia: "anthropic/claude-sonnet-4-6",
5
+ scribe: "anthropic/claude-sonnet-4-6",
6
6
  themis: "openai/gpt-5.5",
7
7
  } as const
8
8
 
@@ -62,6 +62,13 @@ const KNOWN_INPUT_FIELDS = new Set([
62
62
  "observationId",
63
63
  "observationFingerprint",
64
64
  "issueFingerprint",
65
+ "observation_ids",
66
+ "observationIds",
67
+ "observation_count",
68
+ "observationCount",
69
+ "reported_by_agents",
70
+ "reportedByAgents",
71
+ "sources",
65
72
  "elements",
66
73
  "location",
67
74
  ])
@@ -157,6 +164,20 @@ function pushValidationDiagnostics(errors: ValidationError[]): Diagnostic[] {
157
164
  }))
158
165
  }
159
166
 
167
+ function normalizeStringArray(value: unknown): string[] | undefined {
168
+ if (!Array.isArray(value)) return undefined
169
+ const strings = value.filter(
170
+ (item): item is string => typeof item === "string" && item.length > 0,
171
+ )
172
+ return strings.length > 0
173
+ ? Array.from(new Set(strings)).sort((a, b) => a.localeCompare(b))
174
+ : undefined
175
+ }
176
+
177
+ function normalizePositiveInteger(value: unknown): number | undefined {
178
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined
179
+ }
180
+
160
181
  export function normalizeToCanonicalFinding(
161
182
  raw: Finding | Record<string, unknown>,
162
183
  runId: string,
@@ -288,6 +309,16 @@ export function normalizeToCanonicalFinding(
288
309
  observationId,
289
310
  })
290
311
 
312
+ const observationIds =
313
+ normalizeStringArray(input.observation_ids) ?? normalizeStringArray(input.observationIds)
314
+ const reportedByAgents =
315
+ normalizeStringArray(input.reported_by_agents) ?? normalizeStringArray(input.reportedByAgents)
316
+ const sources = normalizeStringArray(input.sources)
317
+ const observationCount =
318
+ normalizePositiveInteger(input.observation_count) ??
319
+ normalizePositiveInteger(input.observationCount) ??
320
+ observationIds?.length
321
+
291
322
  const canonical: CanonicalFinding = {
292
323
  id: observationId,
293
324
  check,
@@ -302,6 +333,10 @@ export function normalizeToCanonicalFinding(
302
333
  issue_fingerprint: issueFingerprint,
303
334
  observation_fingerprint: observationFingerprint,
304
335
  observation_id: observationId,
336
+ observation_ids: observationIds,
337
+ observation_count: observationCount,
338
+ reported_by_agents: reportedByAgents,
339
+ sources,
305
340
  impact: typeof input.impact === "string" && input.impact.length > 0 ? input.impact : undefined,
306
341
  recommendation:
307
342
  typeof input.recommendation === "string" && input.recommendation.length > 0
@@ -85,7 +85,7 @@ export const persistDedupedTool = tool({
85
85
  deduped_findings: tool.schema
86
86
  .string()
87
87
  .describe(
88
- "Serialized JSON array of deduplicated and enriched findings. Each finding should have: check, severity, confidence, description, file, lines, source, impact, recommendation, proofOfConcept.",
88
+ "Serialized JSON array of deduplicated and enriched findings. Each finding should have: check, severity, confidence, description, file, lines, source, impact, recommendation, proofOfConcept, and observation_ids lineage proving which raw findings were merged.",
89
89
  ),
90
90
  },
91
91
  async execute(args, context) {
@@ -860,6 +860,31 @@ function hasDedupLineage(findings: Finding[]): boolean {
860
860
  })
861
861
  }
862
862
 
863
+ function observationIdsForFinding(finding: Finding): string[] {
864
+ const observationIds = (finding as { observation_ids?: unknown }).observation_ids
865
+ if (Array.isArray(observationIds)) {
866
+ return observationIds.filter((id): id is string => typeof id === "string" && id.length > 0)
867
+ }
868
+ return typeof finding.observation_id === "string" && finding.observation_id.length > 0
869
+ ? [finding.observation_id]
870
+ : []
871
+ }
872
+
873
+ function compareObservationLineage(
874
+ eventFindings: Finding[],
875
+ reportFindings: Finding[],
876
+ ): { missing: string[]; extra: string[]; matches: boolean } {
877
+ const expected = new Set(eventFindings.flatMap(observationIdsForFinding))
878
+ const actual = new Set(reportFindings.flatMap(observationIdsForFinding))
879
+ const missing = Array.from(expected)
880
+ .filter((id) => !actual.has(id))
881
+ .sort((a, b) => a.localeCompare(b))
882
+ const extra = Array.from(actual)
883
+ .filter((id) => !expected.has(id))
884
+ .sort((a, b) => a.localeCompare(b))
885
+ return { missing, extra, matches: missing.length === 0 && extra.length === 0 }
886
+ }
887
+
863
888
  export function validateReportQuality(
864
889
  findings: Finding[],
865
890
  policy: QualityGatePolicy,
@@ -1235,7 +1260,9 @@ export async function executeReportGeneration(
1235
1260
  const hasLineage = hasDedupLineage(reportInput.findings)
1236
1261
  const shouldCheckParity = eventFindings.length === inputFindings.length || hasLineage
1237
1262
  const parity = shouldCheckParity
1238
- ? compareIssueFingerprintSets(eventFindings, inputFindings)
1263
+ ? hasLineage
1264
+ ? compareObservationLineage(projectFindings(events), reportInput.findings)
1265
+ : compareIssueFingerprintSets(eventFindings, inputFindings)
1239
1266
  : { missing: [], extra: [], matches: true }
1240
1267
 
1241
1268
  if (!shouldCheckParity) {
@@ -1260,11 +1287,12 @@ export async function executeReportGeneration(
1260
1287
  }
1261
1288
 
1262
1289
  warningBullets.push(`- Finding parity mismatch: ${mismatchSummary}`)
1290
+ const parityLabel = hasLineage ? "observation IDs" : "issue fingerprints"
1263
1291
  if (parity.missing.length > 0) {
1264
- warningBullets.push(`- Missing issue fingerprints: ${parity.missing.join(", ")}`)
1292
+ warningBullets.push(`- Missing ${parityLabel}: ${parity.missing.join(", ")}`)
1265
1293
  }
1266
1294
  if (parity.extra.length > 0) {
1267
- warningBullets.push(`- Extra issue fingerprints: ${parity.extra.join(", ")}`)
1295
+ warningBullets.push(`- Extra ${parityLabel}: ${parity.extra.join(", ")}`)
1268
1296
  }
1269
1297
  }
1270
1298
  } catch (err) {