solidity-argus 0.1.6 → 0.1.8
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/package.json +1 -1
- package/src/agents/argus-prompt.ts +20 -0
- package/src/agents/pythia-prompt.ts +16 -4
- package/src/agents/scribe-prompt.ts +12 -0
- package/src/agents/sentinel-prompt.ts +13 -0
- package/src/create-hooks.ts +56 -28
- package/src/features/error-recovery/session-recovery.ts +7 -1
- package/src/hooks/config-handler.ts +71 -28
- package/src/hooks/event-hook-v2.ts +8 -2
- package/src/hooks/tool-tracking-hook.ts +22 -2
- package/src/hooks/types.ts +0 -1
- package/src/index.ts +4 -5
- package/src/plugin-interface.ts +5 -10
- package/src/hooks/system-prompt-hook.ts +0 -126
package/AGENTS.md
CHANGED
|
@@ -20,18 +20,18 @@ CLI: `argus doctor`, `argus init`, `argus install`.
|
|
|
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
22
|
**Model**: anthropic/claude-sonnet-4-6
|
|
23
|
-
**Tools**: argus_slither_analyze, argus_forge_test, argus_forge_fuzz, argus_analyze_contract, argus_check_patterns
|
|
23
|
+
**Tools**: argus_slither_analyze, argus_forge_test, argus_forge_fuzz, argus_analyze_contract, argus_check_patterns, 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 55 curated vulnerability pattern files. Dispatched by Argus during Vulnerability Research phase.
|
|
29
29
|
**Model**: anthropic/claude-sonnet-4-6
|
|
30
|
-
**Tools**: argus_solodit_search, argus_check_patterns
|
|
30
|
+
**Tools**: argus_solodit_search, argus_check_patterns, 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
36
|
**Model**: anthropic/claude-sonnet-4-6
|
|
37
|
-
**Tools**: argus_generate_report
|
|
37
|
+
**Tools**: argus_generate_report, skill
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solidity-argus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Solidity smart contract security auditing plugin for OpenCode — 4 specialized agents, 8 tools, and a curated vulnerability knowledge base",
|
|
5
5
|
"keywords": ["solidity", "security", "audit", "opencode", "plugin", "smart-contract", "ethereum", "defi", "slither", "foundry"],
|
|
6
6
|
"author": "Apegurus",
|
|
@@ -267,6 +267,26 @@ Your subagents have access to these specialized tools. Know when to delegate eac
|
|
|
267
267
|
- **Purpose**: Updates the local vulnerability database (SCVD).
|
|
268
268
|
- **Note**: Run if you suspect your knowledge base is stale or if the tool reports it's offline.
|
|
269
269
|
|
|
270
|
+
## SKILL SYSTEM
|
|
271
|
+
|
|
272
|
+
You have access to OpenCode Skills through the \`skill\` tool. Skills are specialized knowledge modules and must be used proactively when they improve audit accuracy.
|
|
273
|
+
|
|
274
|
+
- **Curated skill map (load these first)**:
|
|
275
|
+
- **Reconnaissance**: \`amm-dex\`, \`lending-borrowing\`, \`bridges-cross-chain\`
|
|
276
|
+
- **Manual Review**: \`reentrancy\`, \`oracle-manipulation\`, \`access-control\`
|
|
277
|
+
- **Verification**: \`cyfrin-defi-core\`, \`severity-classification\`, \`report-template\`
|
|
278
|
+
|
|
279
|
+
- **Deterministic trigger rules**:
|
|
280
|
+
- If the protocol uses AMM reserves or pool math, load \`amm-dex\` before Attack Surface Mapping.
|
|
281
|
+
- If price feeds or spot prices influence critical state changes, load \`oracle-manipulation\` before severity assessment.
|
|
282
|
+
- If proxy/upgrade patterns are present, load \`cyfrin-best-practices-upgrades\` before final recommendations.
|
|
283
|
+
|
|
284
|
+
- **Trail of Bits skills**:
|
|
285
|
+
- For pre-audit deep context modeling and attack-surface grounding: \`audit-context-building\`
|
|
286
|
+
- For bug family expansion: \`variant-analysis\`
|
|
287
|
+
- For invariant/fuzz strategy: \`property-based-testing\`
|
|
288
|
+
- For token integration risk: \`token-integration-analyzer\` (Trail of Bits building-secure-contracts plugin)
|
|
289
|
+
|
|
270
290
|
## KEY AUDIT PRINCIPLES
|
|
271
291
|
|
|
272
292
|
Adopt these principles to think like a top-tier auditor.
|
|
@@ -90,10 +90,22 @@ You have two primary tools. Master them.
|
|
|
90
90
|
OpenCode has a powerful **Skills** system that allows you to load specialized knowledge modules.
|
|
91
91
|
|
|
92
92
|
**How to use**:
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- **
|
|
93
|
+
- Load a relevant skill before deep research when protocol context is non-trivial.
|
|
94
|
+
- Prioritize vulnerability pattern skills, protocol pattern skills, and reference skills for exploit precedent mapping.
|
|
95
|
+
- Use the \`skill\` tool directly when available to load the exact skill you need.
|
|
96
|
+
- **Curated skill map**:
|
|
97
|
+
- \`reentrancy\`, \`oracle-manipulation\`, \`flash-loan-attacks\`
|
|
98
|
+
- \`lending-borrowing\`, \`amm-dex\`
|
|
99
|
+
- \`exploit-reference\`
|
|
100
|
+
- **Deterministic trigger rules**:
|
|
101
|
+
- If you investigate spot-price dependencies, load \`oracle-manipulation\` first.
|
|
102
|
+
- If capital-efficient attacks or same-block loops are plausible, load \`flash-loan-attacks\` first.
|
|
103
|
+
- If the protocol integrates arbitrary ERC20s, load ToB \`token-integration-analyzer\` (building-secure-contracts plugin) before recommendation drafting.
|
|
104
|
+
- **Examples**:
|
|
105
|
+
- "I am loading \`reentrancy\` to cross-reference known exploit patterns and missed edge cases."
|
|
106
|
+
- "I am loading \`lending-borrowing\` to map lending-specific oracle and liquidation failure modes."
|
|
107
|
+
- "I am loading \`audit-context-building\` (Trail of Bits) to build a line-by-line system model before vulnerability hypothesis generation."
|
|
108
|
+
- You are a generalist researcher. Use Skills to become a specialist on demand.
|
|
97
109
|
|
|
98
110
|
## OUTPUT FORMAT
|
|
99
111
|
|
|
@@ -60,6 +60,18 @@ Before generating the report, verify:
|
|
|
60
60
|
3. **False Positives**: Do not include findings that have been marked as false positives during the analysis phase.
|
|
61
61
|
4. **Clarity**: Is the "Description" easy to understand for a developer? Is the "Recommendation" safe to implement?
|
|
62
62
|
|
|
63
|
+
## SKILL SYSTEM
|
|
64
|
+
|
|
65
|
+
Use the \`skill\` tool when needed to improve report quality and consistency.
|
|
66
|
+
|
|
67
|
+
- **Curated skill map**:
|
|
68
|
+
- \`report-template\`, \`severity-classification\`
|
|
69
|
+
- \`cyfrin-defi-core\`
|
|
70
|
+
- \`exploit-reference\`
|
|
71
|
+
- **Deterministic trigger rules**:
|
|
72
|
+
- If severity wording drifts, load \`severity-classification\` before publishing.
|
|
73
|
+
- If recommendation quality is generic, load \`cyfrin-defi-core\` before final edits.
|
|
74
|
+
|
|
63
75
|
## OUTPUT FORMAT
|
|
64
76
|
|
|
65
77
|
Write the full report in Markdown. Use the standard finding format:
|
|
@@ -87,6 +87,19 @@ You have access to a specific set of tools. Use them effectively.
|
|
|
87
87
|
**Interpretation**:
|
|
88
88
|
- Look at the \`counterexamples\`. They tell you exactly what inputs broke the code.
|
|
89
89
|
|
|
90
|
+
## SKILL SYSTEM
|
|
91
|
+
|
|
92
|
+
Use the \`skill\` tool to load specialized skills before deep verification work.
|
|
93
|
+
|
|
94
|
+
- **Curated skill map**:
|
|
95
|
+
- \`reentrancy\`, \`access-control\`, \`oracle-manipulation\`
|
|
96
|
+
- \`cyfrin-defi-integrations\`, \`severity-classification\`
|
|
97
|
+
- Trail of Bits: \`property-based-testing\`, \`variant-analysis\`
|
|
98
|
+
- **Deterministic trigger rules**:
|
|
99
|
+
- If external calls and mutable state interleave, load \`reentrancy\` before writing PoCs.
|
|
100
|
+
- If privileged flows are central to the finding, load \`access-control\` before severity scoring.
|
|
101
|
+
- If fuzzing strategy is unclear, load ToB \`property-based-testing\` before selecting invariants.
|
|
102
|
+
|
|
90
103
|
## OUTPUT FORMAT
|
|
91
104
|
|
|
92
105
|
Return your findings to Argus in this structured Markdown format. Do not deviate.
|
package/src/create-hooks.ts
CHANGED
|
@@ -2,13 +2,14 @@ import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
|
|
|
2
2
|
import type { ArgusConfig } from "./config/types"
|
|
3
3
|
import type { Managers } from "./managers/types"
|
|
4
4
|
import type { HookName } from "./hooks/types"
|
|
5
|
-
import { createAuditState } from "./state/audit-state"
|
|
6
5
|
import { createConfigHandler } from "./hooks/config-handler"
|
|
7
|
-
import { createSystemPromptHook } from "./hooks/system-prompt-hook"
|
|
8
6
|
import { createCompactionHook } from "./hooks/compaction-hook"
|
|
9
7
|
import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
|
|
10
8
|
import { createEventHookV2 } from "./hooks/event-hook-v2"
|
|
11
9
|
import { safeCreateHook } from "./hooks/safe-create-hook"
|
|
10
|
+
import { createToolOutputTruncator } from "./features/context-monitor"
|
|
11
|
+
import { createSessionRecoveryHandler } from "./features/error-recovery"
|
|
12
|
+
import { createToolErrorRecoveryHandler } from "./features/error-recovery"
|
|
12
13
|
|
|
13
14
|
export type Hooks = Pick<
|
|
14
15
|
PluginHooks,
|
|
@@ -25,45 +26,62 @@ export function createHooks(args: {
|
|
|
25
26
|
projectDir: string
|
|
26
27
|
isHookEnabled: (name: HookName) => boolean
|
|
27
28
|
}): Hooks {
|
|
28
|
-
const { config, projectDir, isHookEnabled } = args
|
|
29
|
+
const { config, managers, projectDir, isHookEnabled } = args
|
|
30
|
+
const { auditStateManager, backgroundManager } = managers
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
|
|
33
|
+
const toolErrorRecoveryHandler = createToolErrorRecoveryHandler()
|
|
34
|
+
const outputTruncator = createToolOutputTruncator()
|
|
33
35
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
|
|
37
|
+
async ({ type, auditState, setAuditState: setState }) => {
|
|
38
|
+
if (type === "session.created") {
|
|
39
|
+
const recoveredState = await auditStateManager.load()
|
|
40
|
+
if (recoveredState) {
|
|
41
|
+
setState(recoveredState)
|
|
42
|
+
}
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (type === "session.idle" && auditState) {
|
|
47
|
+
await auditStateManager.save(auditState)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (type === "session.deleted") {
|
|
52
|
+
await auditStateManager.reset()
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async ({ type, sessionId, setAuditState: setState }) => {
|
|
56
|
+
await sessionRecoveryHandler({ type, sessionId, setAuditState: setState })
|
|
57
|
+
},
|
|
58
|
+
async ({ type }) => {
|
|
59
|
+
if (type === "session.idle") {
|
|
60
|
+
backgroundManager.getActiveCount()
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
])
|
|
40
64
|
|
|
41
|
-
|
|
65
|
+
const initialState = auditStateManager.get()
|
|
66
|
+
if (initialState) {
|
|
67
|
+
setAuditState(initialState)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const compactionHook = isHookEnabled("compaction")
|
|
42
71
|
? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
|
|
43
72
|
: undefined
|
|
44
73
|
|
|
45
74
|
const toolTrackingHook = isHookEnabled("tool-tracking")
|
|
46
|
-
? safeCreateHook(
|
|
47
|
-
() => createToolTrackingHook(auditState, findingStore),
|
|
48
|
-
"tool-tracking"
|
|
49
|
-
)
|
|
75
|
+
? safeCreateHook(() => createToolTrackingHook(getAuditState), "tool-tracking")
|
|
50
76
|
: undefined
|
|
51
77
|
|
|
52
78
|
const safeEventHook = isHookEnabled("event")
|
|
53
79
|
? safeCreateHook(() => eventHook, "event")
|
|
54
80
|
: undefined
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
? async (_input, output) => {
|
|
60
|
-
const block = await systemPromptHook({
|
|
61
|
-
system: output.system.join("\n\n"),
|
|
62
|
-
cwd: projectDir,
|
|
63
|
-
})
|
|
64
|
-
if (block) output.system.push(block)
|
|
65
|
-
}
|
|
66
|
-
: undefined,
|
|
82
|
+
return {
|
|
83
|
+
config: createConfigHandler(config, projectDir),
|
|
84
|
+
"experimental.chat.system.transform": undefined,
|
|
67
85
|
"experimental.session.compacting": compactionHook
|
|
68
86
|
? async (_input, output) => {
|
|
69
87
|
const block = await compactionHook({ summary: output.context.join("\n") })
|
|
@@ -72,11 +90,21 @@ export function createHooks(args: {
|
|
|
72
90
|
: undefined,
|
|
73
91
|
"tool.execute.after": toolTrackingHook
|
|
74
92
|
? async (input, output) => {
|
|
93
|
+
const recoveryHint = toolErrorRecoveryHandler({
|
|
94
|
+
tool: input.tool,
|
|
95
|
+
result: output.output,
|
|
96
|
+
})
|
|
97
|
+
|
|
75
98
|
await toolTrackingHook({
|
|
76
99
|
tool: input.tool,
|
|
77
100
|
args: input.args,
|
|
78
101
|
result: output.output,
|
|
79
102
|
})
|
|
103
|
+
|
|
104
|
+
const outputWithHint = recoveryHint
|
|
105
|
+
? `${output.output}${recoveryHint}`
|
|
106
|
+
: output.output
|
|
107
|
+
output.output = outputTruncator(outputWithHint)
|
|
80
108
|
}
|
|
81
109
|
: undefined,
|
|
82
110
|
event: safeEventHook,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuditState } from "../../state/types"
|
|
1
2
|
import type { AuditStateManager } from "../../managers/types"
|
|
2
3
|
import { createLogger } from "../../shared/logger"
|
|
3
4
|
|
|
@@ -6,7 +7,11 @@ export function createSessionRecoveryHandler(
|
|
|
6
7
|
) {
|
|
7
8
|
const logger = createLogger()
|
|
8
9
|
|
|
9
|
-
return async (event: {
|
|
10
|
+
return async (event: {
|
|
11
|
+
type: string
|
|
12
|
+
sessionId?: string
|
|
13
|
+
setAuditState?: (state: AuditState | null) => void
|
|
14
|
+
}): Promise<void> => {
|
|
10
15
|
if (event.type !== "session.error") return
|
|
11
16
|
|
|
12
17
|
logger.info("Session error detected, attempting state recovery...")
|
|
@@ -14,6 +19,7 @@ export function createSessionRecoveryHandler(
|
|
|
14
19
|
try {
|
|
15
20
|
const recovered = await auditStateManager.load()
|
|
16
21
|
if (recovered) {
|
|
22
|
+
event.setAuditState?.(recovered)
|
|
17
23
|
logger.info(
|
|
18
24
|
`State recovered: phase=${recovered.currentPhase}, findings=${recovered.findings.length}`,
|
|
19
25
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { resolve, join } from "node:path"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
|
-
import { execSync } from "node:child_process"
|
|
5
4
|
import type { Config } from "@opencode-ai/sdk/v2"
|
|
6
5
|
import type { ArgusConfig } from "../config/types"
|
|
7
6
|
import { DEFAULT_MODELS } from "../constants/defaults"
|
|
@@ -13,22 +12,52 @@ import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
|
|
|
13
12
|
|
|
14
13
|
const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
|
|
15
14
|
const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
|
|
15
|
+
let tobCloneInFlight = false
|
|
16
16
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
|
|
18
|
+
const pluginsDir = join(rootDir, "plugins")
|
|
19
|
+
if (!existsSync(pluginsDir)) return []
|
|
20
|
+
|
|
21
|
+
const pluginEntries = readdirSync(pluginsDir, { withFileTypes: true })
|
|
22
|
+
const skillDirs: string[] = []
|
|
23
|
+
|
|
24
|
+
for (const entry of pluginEntries) {
|
|
25
|
+
if (!entry.isDirectory()) continue
|
|
26
|
+
const pluginSkillsDir = join(pluginsDir, entry.name, "skills")
|
|
27
|
+
if (existsSync(pluginSkillsDir)) {
|
|
28
|
+
skillDirs.push(pluginSkillsDir)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return skillDirs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureTrailOfBitsSkills(): string[] {
|
|
36
|
+
if (existsSync(TOB_CACHE_DIR)) {
|
|
37
|
+
return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!tobCloneInFlight) {
|
|
41
|
+
tobCloneInFlight = true
|
|
42
|
+
const cloneProcess = Bun.spawn(
|
|
43
|
+
["git", "clone", "--depth", "1", TOB_REPO_URL, TOB_CACHE_DIR],
|
|
44
|
+
{
|
|
45
|
+
stdin: "ignore",
|
|
46
|
+
stdout: "ignore",
|
|
47
|
+
stderr: "ignore",
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
cloneProcess.exited.finally(() => {
|
|
51
|
+
tobCloneInFlight = false
|
|
23
52
|
})
|
|
24
|
-
return TOB_CACHE_DIR
|
|
25
|
-
} catch (_e) {
|
|
26
|
-
return undefined
|
|
27
53
|
}
|
|
54
|
+
|
|
55
|
+
return []
|
|
28
56
|
}
|
|
29
57
|
|
|
30
58
|
export function createConfigHandler(
|
|
31
|
-
argusConfig: ArgusConfig
|
|
59
|
+
argusConfig: ArgusConfig,
|
|
60
|
+
projectDir: string = process.cwd()
|
|
32
61
|
): (config: Config) => Promise<void> {
|
|
33
62
|
const triggerKnowledgeSync = createKnowledgeSyncHook(argusConfig)
|
|
34
63
|
|
|
@@ -50,6 +79,7 @@ export function createConfigHandler(
|
|
|
50
79
|
pythia: "allow",
|
|
51
80
|
scribe: "allow",
|
|
52
81
|
},
|
|
82
|
+
skill: "allow",
|
|
53
83
|
},
|
|
54
84
|
},
|
|
55
85
|
sentinel: {
|
|
@@ -57,32 +87,35 @@ export function createConfigHandler(
|
|
|
57
87
|
model: argusConfig.agents?.sentinel?.model ?? DEFAULT_MODELS.sentinel,
|
|
58
88
|
description: "Static analysis and testing specialist",
|
|
59
89
|
prompt: SENTINEL_PROMPT,
|
|
60
|
-
|
|
61
|
-
argus_slither_analyze:
|
|
62
|
-
argus_forge_test:
|
|
63
|
-
argus_forge_fuzz:
|
|
64
|
-
argus_analyze_contract:
|
|
65
|
-
argus_check_patterns:
|
|
66
|
-
|
|
90
|
+
permission: {
|
|
91
|
+
argus_slither_analyze: "allow",
|
|
92
|
+
argus_forge_test: "allow",
|
|
93
|
+
argus_forge_fuzz: "allow",
|
|
94
|
+
argus_analyze_contract: "allow",
|
|
95
|
+
argus_check_patterns: "allow",
|
|
96
|
+
skill: "allow",
|
|
97
|
+
},
|
|
67
98
|
},
|
|
68
99
|
pythia: {
|
|
69
100
|
mode: "subagent",
|
|
70
101
|
model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
|
|
71
102
|
description: "Vulnerability researcher",
|
|
72
103
|
prompt: PYTHIA_PROMPT,
|
|
73
|
-
|
|
74
|
-
argus_solodit_search:
|
|
75
|
-
argus_check_patterns:
|
|
76
|
-
|
|
104
|
+
permission: {
|
|
105
|
+
argus_solodit_search: "allow",
|
|
106
|
+
argus_check_patterns: "allow",
|
|
107
|
+
skill: "allow",
|
|
108
|
+
},
|
|
77
109
|
},
|
|
78
110
|
scribe: {
|
|
79
111
|
mode: "subagent",
|
|
80
112
|
model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
|
|
81
113
|
description: "Audit report writer",
|
|
82
114
|
prompt: SCRIBE_PROMPT,
|
|
83
|
-
|
|
84
|
-
argus_generate_report:
|
|
85
|
-
|
|
115
|
+
permission: {
|
|
116
|
+
argus_generate_report: "allow",
|
|
117
|
+
skill: "allow",
|
|
118
|
+
},
|
|
86
119
|
},
|
|
87
120
|
}
|
|
88
121
|
|
|
@@ -101,8 +134,18 @@ export function createConfigHandler(
|
|
|
101
134
|
const skillsPaths = [...(config.skills?.paths ?? [])]
|
|
102
135
|
skillsPaths.push(resolve(import.meta.dir, "../../skills"))
|
|
103
136
|
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
137
|
+
const customSkillsDir = argusConfig.knowledge?.customSkillsDir
|
|
138
|
+
if (customSkillsDir) {
|
|
139
|
+
const resolvedCustomSkillsDir = customSkillsDir.startsWith("/")
|
|
140
|
+
? customSkillsDir
|
|
141
|
+
: resolve(projectDir, customSkillsDir)
|
|
142
|
+
if (existsSync(resolvedCustomSkillsDir)) {
|
|
143
|
+
skillsPaths.push(resolvedCustomSkillsDir)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tobSkillDirs = ensureTrailOfBitsSkills()
|
|
148
|
+
if (tobSkillDirs.length > 0) skillsPaths.push(...tobSkillDirs)
|
|
106
149
|
|
|
107
150
|
config.skills = {
|
|
108
151
|
...(config.skills ?? {}),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuditState
|
|
1
|
+
import type { AuditState } from "../state/types"
|
|
2
2
|
import { createAuditState } from "../state/audit-state"
|
|
3
3
|
import { createLogger } from "../shared/logger"
|
|
4
4
|
|
|
@@ -19,6 +19,7 @@ export type EventSubHandler = (event: {
|
|
|
19
19
|
type: string
|
|
20
20
|
sessionId?: string
|
|
21
21
|
auditState: AuditState | null
|
|
22
|
+
setAuditState: (state: AuditState | null) => void
|
|
22
23
|
}) => Promise<void>
|
|
23
24
|
|
|
24
25
|
export function createEventHookV2(
|
|
@@ -82,7 +83,12 @@ export function createEventHookV2(
|
|
|
82
83
|
|
|
83
84
|
for (const handler of subHandlers) {
|
|
84
85
|
try {
|
|
85
|
-
await handler({
|
|
86
|
+
await handler({
|
|
87
|
+
type,
|
|
88
|
+
sessionId,
|
|
89
|
+
auditState: currentAuditState,
|
|
90
|
+
setAuditState,
|
|
91
|
+
})
|
|
86
92
|
} catch (error) {
|
|
87
93
|
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
88
94
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuditState, FindingSeverity } from "../state/types"
|
|
2
2
|
import type { FindingStore } from "../state/finding-store"
|
|
3
|
+
import { createFindingStore } from "../state/finding-store"
|
|
3
4
|
|
|
4
5
|
type ToolHookInput = {
|
|
5
6
|
tool: string
|
|
@@ -193,14 +194,33 @@ function recordToolExecution(
|
|
|
193
194
|
* Findings are deduplicated via the FindingStore (by check+file+lines).
|
|
194
195
|
*/
|
|
195
196
|
export function createToolTrackingHook(
|
|
196
|
-
|
|
197
|
-
store: FindingStore
|
|
197
|
+
getAuditState: () => AuditState | null
|
|
198
198
|
): (input: ToolHookInput) => Promise<void> {
|
|
199
|
+
const storesByState = new WeakMap<AuditState, FindingStore>()
|
|
200
|
+
|
|
201
|
+
function resolveStateAndStore(): { state: AuditState; store: FindingStore } | null {
|
|
202
|
+
const state = getAuditState()
|
|
203
|
+
if (!state) return null
|
|
204
|
+
|
|
205
|
+
let store = storesByState.get(state)
|
|
206
|
+
if (!store) {
|
|
207
|
+
store = createFindingStore(state)
|
|
208
|
+
storesByState.set(state, store)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { state, store }
|
|
212
|
+
}
|
|
213
|
+
|
|
199
214
|
return async (input: ToolHookInput): Promise<void> => {
|
|
200
215
|
if (!input.tool.startsWith("argus_")) {
|
|
201
216
|
return
|
|
202
217
|
}
|
|
203
218
|
|
|
219
|
+
const resolved = resolveStateAndStore()
|
|
220
|
+
if (!resolved) return
|
|
221
|
+
|
|
222
|
+
const { state: auditState, store } = resolved
|
|
223
|
+
|
|
204
224
|
let parsed: unknown
|
|
205
225
|
try {
|
|
206
226
|
parsed = JSON.parse(input.result)
|
package/src/hooks/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
-
import { spawn } from "node:child_process"
|
|
3
2
|
import { loadArgusConfig } from "./config/loader"
|
|
4
3
|
import { createHookGuard } from "./hooks/hook-system"
|
|
5
4
|
import { createTools } from "./create-tools"
|
|
@@ -8,13 +7,13 @@ import { createManagers } from "./create-managers"
|
|
|
8
7
|
import { createPluginInterface } from "./plugin-interface"
|
|
9
8
|
|
|
10
9
|
function startSoloditMcp(port: number): void {
|
|
11
|
-
const child = spawn("npx",
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const child = Bun.spawn(["npx", "-y", "@lyuboslavlyubenov/solodit-mcp"], {
|
|
11
|
+
stdin: "ignore",
|
|
12
|
+
stdout: "ignore",
|
|
13
|
+
stderr: "ignore",
|
|
14
14
|
env: { ...process.env, PORT: String(port) },
|
|
15
15
|
})
|
|
16
16
|
child.unref()
|
|
17
|
-
child.on("error", () => {})
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
const ArgusPlugin: Plugin = async (ctx) => {
|
package/src/plugin-interface.ts
CHANGED
|
@@ -11,17 +11,12 @@ export function createPluginInterface(args: {
|
|
|
11
11
|
}): PluginReturn {
|
|
12
12
|
const { tools, hooks } = args
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (hooks["experimental.chat.system.transform"]) {
|
|
20
|
-
result["experimental.chat.system.transform"] =
|
|
21
|
-
hooks["experimental.chat.system.transform"]
|
|
22
|
-
}
|
|
14
|
+
const result: PluginReturn = {
|
|
15
|
+
tool: tools,
|
|
16
|
+
config: hooks.config,
|
|
17
|
+
}
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
if (hooks["experimental.session.compacting"]) {
|
|
25
20
|
result["experimental.session.compacting"] =
|
|
26
21
|
hooks["experimental.session.compacting"]
|
|
27
22
|
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import type { AuditState, FindingSeverity } from "../state/types";
|
|
3
|
-
|
|
4
|
-
interface SystemPromptInput {
|
|
5
|
-
system: string;
|
|
6
|
-
cwd: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Checks if the given directory contains a Solidity project
|
|
11
|
-
* by looking for foundry.toml or hardhat.config.{js,ts}
|
|
12
|
-
*/
|
|
13
|
-
async function isSolidityProject(cwd: string): Promise<boolean> {
|
|
14
|
-
const checks = [
|
|
15
|
-
Bun.file(join(cwd, "foundry.toml")).exists(),
|
|
16
|
-
Bun.file(join(cwd, "hardhat.config.js")).exists(),
|
|
17
|
-
Bun.file(join(cwd, "hardhat.config.ts")).exists(),
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
const results = await Promise.all(checks);
|
|
21
|
-
return results.some(Boolean);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Counts findings by severity from the audit state
|
|
26
|
-
*/
|
|
27
|
-
function countFindingsBySeverity(
|
|
28
|
-
findings: AuditState["findings"]
|
|
29
|
-
): Record<FindingSeverity, number> {
|
|
30
|
-
const counts: Record<FindingSeverity, number> = {
|
|
31
|
-
Critical: 0,
|
|
32
|
-
High: 0,
|
|
33
|
-
Medium: 0,
|
|
34
|
-
Low: 0,
|
|
35
|
-
Informational: 0,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
for (const finding of findings) {
|
|
39
|
-
counts[finding.severity]++;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return counts;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Builds the audit state summary section for the injected context
|
|
47
|
-
*/
|
|
48
|
-
function buildAuditStateSummary(state: AuditState | null): string {
|
|
49
|
-
if (!state) {
|
|
50
|
-
return "No active audit session. Use @argus to start an audit.";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const counts = countFindingsBySeverity(state.findings);
|
|
54
|
-
const scopeList =
|
|
55
|
-
state.scope.length > 0 ? state.scope.join(", ") : "not defined";
|
|
56
|
-
const reviewedList =
|
|
57
|
-
state.contractsReviewed.length > 0
|
|
58
|
-
? state.contractsReviewed.join(", ")
|
|
59
|
-
: "none yet";
|
|
60
|
-
|
|
61
|
-
return [
|
|
62
|
-
`Phase: ${state.currentPhase}`,
|
|
63
|
-
`Scope: ${scopeList}`,
|
|
64
|
-
`Contracts reviewed: ${reviewedList}`,
|
|
65
|
-
`Findings: ${state.findings.length} total — Critical: ${counts.Critical}, High: ${counts.High}, Medium: ${counts.Medium}, Low: ${counts.Low}, Info: ${counts.Informational}`,
|
|
66
|
-
].join("\n");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Builds the full audit context block to inject into the system prompt.
|
|
71
|
-
* Designed to be concise (500-800 tokens).
|
|
72
|
-
*/
|
|
73
|
-
function buildAuditContextBlock(state: AuditState | null): string {
|
|
74
|
-
return `
|
|
75
|
-
<argus-context>
|
|
76
|
-
## Solidity Audit Context
|
|
77
|
-
|
|
78
|
-
### Severity Classification
|
|
79
|
-
- **Critical**: Direct theft/freezing of funds, unauthorized admin access, contract destruction
|
|
80
|
-
- **High**: Indirect fund loss, business logic manipulation, DoS on critical functions
|
|
81
|
-
- **Medium**: Degraded functionality, edge-case bugs, partial DoS, poor validation
|
|
82
|
-
- **Low**: Code quality issues, suboptimal patterns, missing events, minor logic issues
|
|
83
|
-
- **Informational**: Gas optimizations, style suggestions, best practices, non-security notes
|
|
84
|
-
|
|
85
|
-
### Available Argus Tools
|
|
86
|
-
- \`argus_slither_analyze\`: Run Slither static analysis on Solidity codebase
|
|
87
|
-
- \`argus_forge_test\`: Execute Foundry/Forge tests for vulnerability verification
|
|
88
|
-
- \`argus_forge_fuzz\`: Fuzz specific functions to discover edge cases
|
|
89
|
-
- \`argus_analyze_contract\`: Generate deep structural profile of a contract
|
|
90
|
-
- \`argus_check_patterns\`: Scan code against known vulnerability pattern library
|
|
91
|
-
- \`argus_solodit_search\`: Search real-world audit reports and known vulnerabilities
|
|
92
|
-
- \`argus_generate_report\`: Compile findings into structured audit report
|
|
93
|
-
- \`argus_sync_knowledge\`: Update local vulnerability database (SCVD)
|
|
94
|
-
|
|
95
|
-
### Audit State
|
|
96
|
-
${buildAuditStateSummary(state)}
|
|
97
|
-
|
|
98
|
-
### Quick Reference
|
|
99
|
-
Use @argus for full audits, @sentinel for testing, @pythia for research, @scribe for reports.
|
|
100
|
-
Severity must follow classification above. Do not inflate severity.
|
|
101
|
-
</argus-context>`.trim();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Factory function that creates a system prompt transform hook.
|
|
106
|
-
* The hook injects Solidity audit context when working in a Solidity project.
|
|
107
|
-
*
|
|
108
|
-
* @param getAuditState - Accessor function for current audit state (may return null)
|
|
109
|
-
* @returns Async transform function compatible with OpenCode's experimental.chat.system.transform
|
|
110
|
-
*/
|
|
111
|
-
export function createSystemPromptHook(
|
|
112
|
-
getAuditState: () => AuditState | null
|
|
113
|
-
): (input: SystemPromptInput) => Promise<string | null> {
|
|
114
|
-
return async (input: SystemPromptInput): Promise<string | null> => {
|
|
115
|
-
const isSolidity = await isSolidityProject(input.cwd);
|
|
116
|
-
|
|
117
|
-
if (!isSolidity) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const auditState = getAuditState();
|
|
122
|
-
return buildAuditContextBlock(auditState);
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export default createSystemPromptHook;
|