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 +3 -3
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/agents/argus-prompt.ts +2 -2
- package/src/agents/scribe-prompt.ts +2 -1
- package/src/cli/commands/doctor.ts +9 -15
- package/src/constants/defaults.ts +3 -3
- package/src/state/adapters.ts +35 -0
- package/src/tools/persist-deduped-tool.ts +1 -1
- package/src/tools/report-generator-tool.ts +31 -3
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-
|
|
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-
|
|
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-
|
|
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-
|
|
70
|
-
| `@pythia` | Vulnerability researcher | claude-sonnet-4-
|
|
71
|
-
| `@scribe` | Audit report writer | claude-sonnet-4-
|
|
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-
|
|
289
|
-
"pythia": { "model": "anthropic/claude-sonnet-4-
|
|
290
|
-
"scribe": { "model": "anthropic/claude-sonnet-4-
|
|
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.
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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-
|
|
4
|
-
pythia: "anthropic/claude-sonnet-4-
|
|
5
|
-
scribe: "anthropic/claude-sonnet-4-
|
|
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
|
|
package/src/state/adapters.ts
CHANGED
|
@@ -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
|
-
?
|
|
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
|
|
1292
|
+
warningBullets.push(`- Missing ${parityLabel}: ${parity.missing.join(", ")}`)
|
|
1265
1293
|
}
|
|
1266
1294
|
if (parity.extra.length > 0) {
|
|
1267
|
-
warningBullets.push(`- Extra
|
|
1295
|
+
warningBullets.push(`- Extra ${parityLabel}: ${parity.extra.join(", ")}`)
|
|
1268
1296
|
}
|
|
1269
1297
|
}
|
|
1270
1298
|
} catch (err) {
|