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.
- package/README.md +161 -1
- package/package.json +5 -2
- package/skills/README.md +63 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +11 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +11 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/patterns/access-control.yaml +31 -0
- package/skills/patterns/erc4626.yaml +29 -0
- package/skills/patterns/flash-loan.yaml +20 -0
- package/skills/patterns/oracle.yaml +30 -0
- package/skills/patterns/proxy.yaml +30 -0
- package/skills/patterns/reentrancy.yaml +30 -0
- package/skills/patterns/signature.yaml +31 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
- package/src/agents/argus-prompt.ts +7 -7
- package/src/agents/pythia-prompt.ts +11 -11
- package/src/agents/scribe-prompt.ts +6 -6
- package/src/agents/sentinel-prompt.ts +7 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +9 -5
- package/src/cli/commands/doctor.ts +274 -16
- package/src/cli/commands/init.ts +5 -5
- package/src/cli/commands/install.ts +5 -5
- package/src/cli/commands/lint-skills.ts +114 -0
- package/src/cli/tui-prompts.ts +4 -2
- package/src/config/schema.ts +2 -0
- package/src/create-hooks.ts +141 -32
- package/src/create-tools.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +7 -1
- package/src/features/error-recovery/tool-error-recovery.ts +74 -19
- package/src/features/persistent-state/audit-state-manager.ts +36 -13
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +22 -9
- package/src/hooks/context-budget.ts +45 -0
- package/src/hooks/event-hook-v2.ts +8 -2
- package/src/hooks/event-hook.ts +5 -4
- package/src/hooks/knowledge-sync-hook.ts +2 -1
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +4 -5
- package/src/hooks/system-prompt-hook.ts +92 -221
- package/src/hooks/tool-tracking-hook.ts +108 -9
- package/src/hooks/types.ts +0 -1
- package/src/index.ts +28 -6
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +37 -10
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +53 -3
- package/src/knowledge/scvd-sync.ts +205 -34
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/plugin-interface.ts +11 -3
- package/src/shared/binary-utils.ts +1 -0
- package/src/shared/logger.ts +78 -17
- package/src/skills/argus-skill-resolver.ts +226 -0
- package/src/skills/skill-schema.ts +98 -0
- package/src/state/audit-state.ts +2 -0
- package/src/state/types.ts +32 -1
- package/src/tools/argus-skill-load-tool.ts +73 -0
- package/src/tools/pattern-checker-tool.ts +56 -12
- package/src/tools/pattern-loader.ts +183 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/report-generator-tool.ts +134 -11
- package/src/tools/slither-tool.ts +61 -19
- package/src/tools/solodit-search-tool.ts +92 -14
- package/src/utils/audit-artifact-detector.ts +119 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +128 -26
- package/src/utils/solidity-parser.ts +20 -4
- package/src/utils/solodit-health.ts +29 -0
package/src/create-hooks.ts
CHANGED
|
@@ -2,55 +2,154 @@ 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"
|
|
9
|
+
import { createAgentTracker } from "./hooks/agent-tracker"
|
|
10
|
+
import { createSystemPromptHook } from "./hooks/system-prompt-hook"
|
|
11
11
|
import { safeCreateHook } from "./hooks/safe-create-hook"
|
|
12
|
+
import { createContextMonitor, createToolOutputTruncator } from "./features/context-monitor"
|
|
13
|
+
import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
|
|
14
|
+
import { getTokenBudgetForAgent } from "./hooks/context-budget"
|
|
15
|
+
import { createSessionRecoveryHandler } from "./features/error-recovery"
|
|
16
|
+
import { createToolErrorRecoveryHandler } from "./features/error-recovery"
|
|
17
|
+
import { detectProject } from "./utils/project-detector"
|
|
18
|
+
import type { ProjectConfig } from "./utils/project-detector"
|
|
19
|
+
import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
|
|
20
|
+
import type { ReconContext } from "./hooks/recon-context-builder"
|
|
21
|
+
import { buildReconContextBlock } from "./hooks/recon-context-builder"
|
|
22
|
+
import type { AuditState } from "./state/types"
|
|
23
|
+
|
|
24
|
+
let latestAgentTracker: ReturnType<typeof createAgentTracker> | undefined
|
|
25
|
+
|
|
26
|
+
export function getAgentForSession(sessionID: string): string | undefined {
|
|
27
|
+
return latestAgentTracker?.getAgentForSession(sessionID)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isArgusAgent(sessionID: string): boolean {
|
|
31
|
+
return latestAgentTracker?.isArgusAgent(sessionID) ?? false
|
|
32
|
+
}
|
|
12
33
|
|
|
13
34
|
export type Hooks = Pick<
|
|
14
35
|
PluginHooks,
|
|
15
36
|
| "config"
|
|
37
|
+
| "chat.params"
|
|
38
|
+
| "chat.message"
|
|
16
39
|
| "experimental.chat.system.transform"
|
|
17
40
|
| "experimental.session.compacting"
|
|
18
41
|
| "tool.execute.after"
|
|
19
42
|
| "event"
|
|
20
43
|
>
|
|
21
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Creates the hook handlers for the Argus plugin.
|
|
47
|
+
*
|
|
48
|
+
* Context Delivery Strategy:
|
|
49
|
+
* - Prompt: Static agent identity (src/agents/*-prompt.ts) — methodology, personality, tool instructions
|
|
50
|
+
* - Hook: Dynamic state injection via experimental.chat.system.transform — audit progress, findings, phase
|
|
51
|
+
* - Skill-load: On-demand knowledge via argus_skill_load tool — vulnerability patterns, protocol knowledge
|
|
52
|
+
*
|
|
53
|
+
* The system.transform hook injects dynamic audit context only for Argus-family agents
|
|
54
|
+
* (argus, sentinel, pythia, scribe). Non-audit agents receive no injection.
|
|
55
|
+
*/
|
|
22
56
|
export function createHooks(args: {
|
|
23
57
|
config: ArgusConfig
|
|
24
58
|
managers: Managers
|
|
25
59
|
projectDir: string
|
|
26
60
|
isHookEnabled: (name: HookName) => boolean
|
|
27
61
|
}): Hooks {
|
|
28
|
-
const { config, projectDir, isHookEnabled } = args
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
const { config, managers, projectDir, isHookEnabled } = args
|
|
63
|
+
const { auditStateManager, backgroundManager } = managers
|
|
64
|
+
const agentTracker = createAgentTracker()
|
|
65
|
+
latestAgentTracker = agentTracker
|
|
66
|
+
|
|
67
|
+
const contextMonitor = createContextMonitor()
|
|
68
|
+
const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
|
|
69
|
+
let auditStateGetter: (() => AuditState | null) | undefined
|
|
70
|
+
const toolErrorRecoveryHandler = createToolErrorRecoveryHandler(() => auditStateGetter?.() ?? null)
|
|
71
|
+
const outputTruncator = createToolOutputTruncator()
|
|
72
|
+
|
|
73
|
+
const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
|
|
74
|
+
async ({ type, sessionId, auditState, setAuditState: setState }) => {
|
|
75
|
+
if (type === "session.created") {
|
|
76
|
+
const recoveredState = await auditStateManager.load()
|
|
77
|
+
if (recoveredState) {
|
|
78
|
+
setState(recoveredState)
|
|
79
|
+
}
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (type === "session.idle" && auditState) {
|
|
84
|
+
await auditStateManager.save(auditState)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (type === "session.deleted") {
|
|
89
|
+
if (sessionId) {
|
|
90
|
+
agentTracker.clearSession(sessionId)
|
|
91
|
+
}
|
|
92
|
+
await auditStateManager.reset()
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
async ({ type, sessionId, setAuditState: setState }) => {
|
|
96
|
+
await sessionRecoveryHandler({ type, sessionId, setAuditState: setState })
|
|
97
|
+
},
|
|
98
|
+
async ({ type }) => {
|
|
99
|
+
if (type === "session.idle") {
|
|
100
|
+
backgroundManager.getActiveCount()
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
auditStateGetter = getAuditState
|
|
106
|
+
|
|
107
|
+
const initialState = auditStateManager.get()
|
|
108
|
+
if (initialState) {
|
|
109
|
+
setAuditState(initialState)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const auditEnforcer = createAuditEnforcer()
|
|
113
|
+
|
|
114
|
+
const systemPromptHook = createSystemPromptHook({
|
|
115
|
+
getAuditState,
|
|
116
|
+
getAgentForSession: agentTracker.getAgentForSession,
|
|
117
|
+
isArgusAgent: agentTracker.isArgusAgent,
|
|
118
|
+
getContextPressure: (systemText: string) => {
|
|
119
|
+
const status = contextMonitor.getContextStatus(systemText, getAuditState())
|
|
120
|
+
return status.usage
|
|
121
|
+
},
|
|
122
|
+
getTokenBudget: getTokenBudgetForAgent,
|
|
123
|
+
getEnforcerReminder: auditEnforcer,
|
|
124
|
+
getReconBlock: () => buildReconContextBlock({
|
|
125
|
+
projectConfig: reconProjectConfig,
|
|
126
|
+
dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
|
|
127
|
+
auditArtifacts: detectAuditArtifacts(projectDir),
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
let reconProjectConfig: ProjectConfig | null = null
|
|
132
|
+
|
|
133
|
+
detectProject(projectDir)
|
|
134
|
+
.then((config) => {
|
|
135
|
+
reconProjectConfig = config
|
|
136
|
+
})
|
|
137
|
+
.catch(() => {
|
|
138
|
+
// Silent fallback — audit artifacts remain available
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const getReconContext = (): ReconContext => ({
|
|
142
|
+
projectConfig: reconProjectConfig,
|
|
143
|
+
dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
|
|
144
|
+
auditArtifacts: detectAuditArtifacts(projectDir),
|
|
145
|
+
})
|
|
44
146
|
|
|
45
147
|
const compactionHook = isHookEnabled("compaction")
|
|
46
|
-
? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
|
|
148
|
+
? safeCreateHook(() => createCompactionHook(getAuditState, getReconContext), "compaction")
|
|
47
149
|
: undefined
|
|
48
150
|
|
|
49
151
|
const toolTrackingHook = isHookEnabled("tool-tracking")
|
|
50
|
-
? safeCreateHook(
|
|
51
|
-
() => createToolTrackingHook(auditState, findingStore),
|
|
52
|
-
"tool-tracking"
|
|
53
|
-
)
|
|
152
|
+
? safeCreateHook(() => createToolTrackingHook(getAuditState), "tool-tracking")
|
|
54
153
|
: undefined
|
|
55
154
|
|
|
56
155
|
const safeEventHook = isHookEnabled("event")
|
|
@@ -59,15 +158,15 @@ export function createHooks(args: {
|
|
|
59
158
|
|
|
60
159
|
return {
|
|
61
160
|
config: createConfigHandler(config, projectDir),
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
161
|
+
"chat.params": async (input) => {
|
|
162
|
+
agentTracker.chatParamsHook(input)
|
|
163
|
+
},
|
|
164
|
+
"chat.message": async (input) => {
|
|
165
|
+
agentTracker.chatMessageHook(input)
|
|
166
|
+
},
|
|
167
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
168
|
+
await systemPromptHook(input, output)
|
|
169
|
+
},
|
|
71
170
|
"experimental.session.compacting": compactionHook
|
|
72
171
|
? async (_input, output) => {
|
|
73
172
|
const block = await compactionHook({ summary: output.context.join("\n") })
|
|
@@ -76,11 +175,21 @@ export function createHooks(args: {
|
|
|
76
175
|
: undefined,
|
|
77
176
|
"tool.execute.after": toolTrackingHook
|
|
78
177
|
? async (input, output) => {
|
|
178
|
+
const recoveryHint = toolErrorRecoveryHandler({
|
|
179
|
+
tool: input.tool,
|
|
180
|
+
result: output.output,
|
|
181
|
+
})
|
|
182
|
+
|
|
79
183
|
await toolTrackingHook({
|
|
80
184
|
tool: input.tool,
|
|
81
185
|
args: input.args,
|
|
82
186
|
result: output.output,
|
|
83
187
|
})
|
|
188
|
+
|
|
189
|
+
const outputWithHint = recoveryHint
|
|
190
|
+
? `${output.output}${recoveryHint}`
|
|
191
|
+
: output.output
|
|
192
|
+
output.output = outputTruncator(outputWithHint)
|
|
84
193
|
}
|
|
85
194
|
: undefined,
|
|
86
195
|
event: safeEventHook,
|
package/src/create-tools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { patternCheckerTool } from "./tools/pattern-checker-tool"
|
|
|
8
8
|
import { soloditSearchTool } from "./tools/solodit-search-tool"
|
|
9
9
|
import { reportGeneratorTool } from "./tools/report-generator-tool"
|
|
10
10
|
import { syncKnowledgeTool } from "./tools/sync-knowledge-tool"
|
|
11
|
+
import { argusSkillLoadTool } from "./tools/argus-skill-load-tool"
|
|
11
12
|
|
|
12
13
|
export function createTools(
|
|
13
14
|
config: ArgusConfig,
|
|
@@ -18,6 +19,7 @@ export function createTools(
|
|
|
18
19
|
argus_forge_fuzz: forgeFuzzTool,
|
|
19
20
|
argus_analyze_contract: contractAnalyzerTool,
|
|
20
21
|
argus_check_patterns: patternCheckerTool,
|
|
22
|
+
argus_skill_load: argusSkillLoadTool,
|
|
21
23
|
argus_generate_report: reportGeneratorTool,
|
|
22
24
|
argus_sync_knowledge: syncKnowledgeTool,
|
|
23
25
|
}
|
|
@@ -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,15 +1,60 @@
|
|
|
1
1
|
import { createLogger } from "../../shared/logger"
|
|
2
|
+
import type { AuditState } from "../../state/types"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
solodit: "Check network connectivity or Solodit API status",
|
|
7
|
-
scvd: "Check SCVD API at https://api.scvd.dev — may be temporarily unavailable",
|
|
4
|
+
type ToolFallbackEntry = {
|
|
5
|
+
install: string
|
|
6
|
+
fallback: string
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
const
|
|
9
|
+
const TOOL_FALLBACKS: Record<string, ToolFallbackEntry> = {
|
|
10
|
+
slither: {
|
|
11
|
+
install: "pip install slither-analyzer",
|
|
12
|
+
fallback:
|
|
13
|
+
"Slither is unavailable. PROCEED with the audit using `argus_analyze_contract` for structural profiling and `argus_check_patterns` for vulnerability scanning. Note in the final report: \"Automated static analysis (Slither) was unavailable; manual review intensity increased.\"",
|
|
14
|
+
},
|
|
15
|
+
forge: {
|
|
16
|
+
install: "curl -L https://foundry.paradigm.xyz | bash && foundryup",
|
|
17
|
+
fallback:
|
|
18
|
+
"Foundry/Forge is unavailable. SKIP automated testing and fuzzing. Verify findings through manual code tracing and static analysis. Note in the final report: \"Dynamic testing (Forge) was unavailable; findings verified via manual analysis.\"",
|
|
19
|
+
},
|
|
20
|
+
solodit: {
|
|
21
|
+
install: "",
|
|
22
|
+
fallback:
|
|
23
|
+
"Solodit API is unreachable. PROCEED using `argus_check_patterns` with local vulnerability rules. Note in the final report: \"External vulnerability databases were inaccessible; research limited to local patterns.\"",
|
|
24
|
+
},
|
|
25
|
+
scvd: {
|
|
26
|
+
install: "",
|
|
27
|
+
fallback:
|
|
28
|
+
"SCVD API is unavailable. PROCEED with local patterns and Solodit search if available.",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VIA_IR_HINT =
|
|
33
|
+
"Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
|
|
34
|
+
|
|
35
|
+
function isToolUnavailable(lowerResult: string): boolean {
|
|
36
|
+
return (
|
|
37
|
+
lowerResult.includes("enoent") ||
|
|
38
|
+
lowerResult.includes("not found") ||
|
|
39
|
+
lowerResult.includes("not installed")
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isToolError(lowerResult: string): boolean {
|
|
44
|
+
return (
|
|
45
|
+
isToolUnavailable(lowerResult) ||
|
|
46
|
+
lowerResult.includes("command failed") ||
|
|
47
|
+
lowerResult.includes("error:")
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveToolBase(tool: string): string {
|
|
52
|
+
return tool.replace("argus_", "").split("_")[0] ?? ""
|
|
53
|
+
}
|
|
11
54
|
|
|
12
|
-
export function createToolErrorRecoveryHandler(
|
|
55
|
+
export function createToolErrorRecoveryHandler(
|
|
56
|
+
getAuditState?: () => AuditState | null,
|
|
57
|
+
) {
|
|
13
58
|
const logger = createLogger()
|
|
14
59
|
|
|
15
60
|
return (toolResult: { tool: string; result: string }): string | null => {
|
|
@@ -27,22 +72,32 @@ export function createToolErrorRecoveryHandler() {
|
|
|
27
72
|
return `\n[Argus Recovery Hint] ${VIA_IR_HINT}`
|
|
28
73
|
}
|
|
29
74
|
|
|
30
|
-
|
|
31
|
-
lowerResult.includes("enoent") ||
|
|
32
|
-
lowerResult.includes("not found") ||
|
|
33
|
-
lowerResult.includes("command failed") ||
|
|
34
|
-
lowerResult.includes("error:")
|
|
75
|
+
if (!isToolError(lowerResult)) return null
|
|
35
76
|
|
|
36
|
-
|
|
77
|
+
const toolBase = resolveToolBase(tool)
|
|
78
|
+
const entry = TOOL_FALLBACKS[toolBase]
|
|
79
|
+
if (!entry) return null
|
|
37
80
|
|
|
38
|
-
const
|
|
39
|
-
|
|
81
|
+
const unavailable = isToolUnavailable(lowerResult)
|
|
82
|
+
|
|
83
|
+
if (unavailable && getAuditState) {
|
|
84
|
+
const state = getAuditState()
|
|
85
|
+
if (state) {
|
|
86
|
+
state.unavailableTools ??= []
|
|
87
|
+
if (!state.unavailableTools.includes(toolBase)) {
|
|
88
|
+
state.unavailableTools.push(toolBase)
|
|
89
|
+
logger.info(`Recorded ${toolBase} as unavailable — fallback activated`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
40
93
|
|
|
41
|
-
if (
|
|
42
|
-
logger.info(`Tool
|
|
43
|
-
return `\n[Argus
|
|
94
|
+
if (unavailable) {
|
|
95
|
+
logger.info(`Tool unavailable fallback for ${tool}`)
|
|
96
|
+
return `\n[Argus Fallback] ${entry.fallback}`
|
|
44
97
|
}
|
|
45
98
|
|
|
46
|
-
|
|
99
|
+
const installHint = entry.install ? ` (install: ${entry.install})` : ""
|
|
100
|
+
logger.info(`Tool error recovery hint for ${tool}`)
|
|
101
|
+
return `\n[Argus Recovery Hint] ${toolBase} error${installHint}. ${entry.fallback}`
|
|
47
102
|
}
|
|
48
103
|
}
|
|
@@ -7,7 +7,7 @@ import { createLogger } from "../../shared/logger";
|
|
|
7
7
|
|
|
8
8
|
const STATE_FILE_DIR = ".opencode";
|
|
9
9
|
const STATE_FILE_NAME = "argus-state.json";
|
|
10
|
-
const STATE_VERSION = "
|
|
10
|
+
const STATE_VERSION = "2";
|
|
11
11
|
|
|
12
12
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
13
13
|
return typeof value === "object" && value !== null;
|
|
@@ -39,9 +39,11 @@ function isPersistentAuditState(value: unknown): value is PersistentAuditState {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const hasSupportedVersion = value.version === "1" || value.version === "2";
|
|
43
|
+
|
|
42
44
|
return (
|
|
43
45
|
typeof value.savedAt === "number" &&
|
|
44
|
-
|
|
46
|
+
hasSupportedVersion &&
|
|
45
47
|
typeof value.filePath === "string"
|
|
46
48
|
);
|
|
47
49
|
}
|
|
@@ -69,7 +71,17 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
69
71
|
return null;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
const { savedAt: _savedAt, version
|
|
74
|
+
const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed;
|
|
75
|
+
|
|
76
|
+
if (version === "1") {
|
|
77
|
+
if (!state.soloditResults) {
|
|
78
|
+
state.soloditResults = [];
|
|
79
|
+
}
|
|
80
|
+
if (!state.fuzzCounterexamples) {
|
|
81
|
+
state.fuzzCounterexamples = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
currentState = state;
|
|
74
86
|
return currentState;
|
|
75
87
|
} catch (_error) {
|
|
@@ -77,20 +89,31 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
let saveInFlight = false;
|
|
93
|
+
|
|
80
94
|
async function save(state: AuditState): Promise<void> {
|
|
81
95
|
currentState = state;
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
savedAt: Date.now(),
|
|
86
|
-
version: STATE_VERSION,
|
|
87
|
-
filePath: stateFilePath,
|
|
88
|
-
};
|
|
97
|
+
if (saveInFlight) return;
|
|
98
|
+
saveInFlight = true;
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
try {
|
|
101
|
+
const persistentState: PersistentAuditState = {
|
|
102
|
+
...state,
|
|
103
|
+
savedAt: Date.now(),
|
|
104
|
+
version: STATE_VERSION,
|
|
105
|
+
filePath: stateFilePath,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`;
|
|
109
|
+
await mkdir(dirname(stateFilePath), { recursive: true });
|
|
110
|
+
await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
|
|
111
|
+
await rename(tempFilePath, stateFilePath);
|
|
112
|
+
} catch {
|
|
113
|
+
// Non-critical: state persistence is best-effort
|
|
114
|
+
} finally {
|
|
115
|
+
saveInFlight = false;
|
|
116
|
+
}
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
function get(): AuditState {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
type ChatParamsInput = Parameters<NonNullable<PluginHooks["chat.params"]>>[0] & {
|
|
4
|
+
agent?: string
|
|
5
|
+
}
|
|
6
|
+
type ChatMessageInput = Parameters<NonNullable<PluginHooks["chat.message"]>>[0]
|
|
7
|
+
|
|
8
|
+
const ARGUS_FAMILY = new Set(["argus", "sentinel", "pythia", "scribe"])
|
|
9
|
+
|
|
10
|
+
export type AgentTracker = ReturnType<typeof createAgentTracker>
|
|
11
|
+
|
|
12
|
+
export function createAgentTracker() {
|
|
13
|
+
const sessions = new Map<string, string>()
|
|
14
|
+
|
|
15
|
+
const trackSession = (sessionID: string, agent?: string): void => {
|
|
16
|
+
if (!agent) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
sessions.set(sessionID, agent)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
chatParamsHook: (input: ChatParamsInput): void => {
|
|
25
|
+
trackSession(input.sessionID, input.agent)
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
chatMessageHook: (input: ChatMessageInput): void => {
|
|
29
|
+
trackSession(input.sessionID, input.agent)
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getAgentForSession: (sessionID: string): string | undefined => {
|
|
33
|
+
return sessions.get(sessionID)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
isArgusAgent: (sessionID: string): boolean => {
|
|
37
|
+
const agent = sessions.get(sessionID)
|
|
38
|
+
if (!agent) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ARGUS_FAMILY.has(agent)
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
clearSession: (sessionID: string): void => {
|
|
46
|
+
sessions.delete(sessionID)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
getTrackedSessions: (): Map<string, string> => {
|
|
50
|
+
return sessions
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,50 +1,59 @@
|
|
|
1
1
|
import type { AuditState, FindingSeverity } from "../state/types"
|
|
2
|
+
import type { ReconContext } from "./recon-context-builder"
|
|
3
|
+
import { buildReconContextBlock } from "./recon-context-builder"
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
* Creates a compaction hook that serializes audit state into XML format
|
|
5
|
-
* so findings survive context window compression.
|
|
6
|
-
*
|
|
7
|
-
* The returned hook is called by OpenCode's `experimental.session.compacting`
|
|
8
|
-
* event, receiving `{ summary: string }` and returning the enriched summary.
|
|
9
|
-
*/
|
|
10
5
|
export function createCompactionHook(
|
|
11
|
-
getAuditState: () => AuditState | null
|
|
6
|
+
getAuditState: () => AuditState | null,
|
|
7
|
+
getReconContext?: () => ReconContext | null,
|
|
12
8
|
): (input: { summary: string }) => Promise<string | null> {
|
|
13
9
|
return async (_input: { summary: string }): Promise<string | null> => {
|
|
14
10
|
const state = getAuditState()
|
|
15
|
-
if (!state) {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
11
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
|
|
14
|
+
if (state) {
|
|
15
|
+
const severityCounts: Record<FindingSeverity, number> = {
|
|
16
|
+
Critical: 0,
|
|
17
|
+
High: 0,
|
|
18
|
+
Medium: 0,
|
|
19
|
+
Low: 0,
|
|
20
|
+
Informational: 0,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const finding of state.findings) {
|
|
24
|
+
severityCounts[finding.severity]++
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
|
|
28
|
+
const contracts = state.contractsReviewed.join(", ")
|
|
29
|
+
const started = new Date(state.startTime).toISOString()
|
|
30
|
+
|
|
31
|
+
parts.push(
|
|
32
|
+
[
|
|
33
|
+
"<argus-audit-state>",
|
|
34
|
+
`Phase: ${state.currentPhase}`,
|
|
35
|
+
`Contracts Reviewed: ${contracts}`,
|
|
36
|
+
"Findings:",
|
|
37
|
+
` Critical: ${severityCounts.Critical}`,
|
|
38
|
+
` High: ${severityCounts.High}`,
|
|
39
|
+
` Medium: ${severityCounts.Medium}`,
|
|
40
|
+
` Low: ${severityCounts.Low}`,
|
|
41
|
+
` Informational: ${severityCounts.Informational}`,
|
|
42
|
+
`Tools Executed: ${toolNames}`,
|
|
43
|
+
`Started: ${started}`,
|
|
44
|
+
"</argus-audit-state>",
|
|
45
|
+
].join("\n"),
|
|
46
|
+
)
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
if (getReconContext) {
|
|
50
|
+
const recon = getReconContext()
|
|
51
|
+
if (recon) {
|
|
52
|
+
const reconBlock = buildReconContextBlock(recon)
|
|
53
|
+
if (reconBlock) parts.push(reconBlock)
|
|
54
|
+
}
|
|
29
55
|
}
|
|
30
56
|
|
|
31
|
-
|
|
32
|
-
const contracts = state.contractsReviewed.join(", ")
|
|
33
|
-
const started = new Date(state.startTime).toISOString()
|
|
34
|
-
|
|
35
|
-
return [
|
|
36
|
-
"<argus-audit-state>",
|
|
37
|
-
`Phase: ${state.currentPhase}`,
|
|
38
|
-
`Contracts Reviewed: ${contracts}`,
|
|
39
|
-
"Findings:",
|
|
40
|
-
` Critical: ${severityCounts.Critical}`,
|
|
41
|
-
` High: ${severityCounts.High}`,
|
|
42
|
-
` Medium: ${severityCounts.Medium}`,
|
|
43
|
-
` Low: ${severityCounts.Low}`,
|
|
44
|
-
` Informational: ${severityCounts.Informational}`,
|
|
45
|
-
`Tools Executed: ${toolNames}`,
|
|
46
|
-
`Started: ${started}`,
|
|
47
|
-
"</argus-audit-state>",
|
|
48
|
-
].join("\n")
|
|
57
|
+
return parts.length > 0 ? parts.join("\n") : null
|
|
49
58
|
}
|
|
50
59
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { resolve, join } from "node:path"
|
|
2
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,6 +12,7 @@ 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
17
|
function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
|
|
18
18
|
const pluginsDir = join(rootDir, "plugins")
|
|
@@ -33,16 +33,26 @@ function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function ensureTrailOfBitsSkills(): string[] {
|
|
36
|
-
if (existsSync(TOB_CACHE_DIR))
|
|
37
|
-
try {
|
|
38
|
-
execSync(`git clone --depth 1 ${TOB_REPO_URL} "${TOB_CACHE_DIR}"`, {
|
|
39
|
-
stdio: "ignore",
|
|
40
|
-
timeout: 30_000,
|
|
41
|
-
})
|
|
36
|
+
if (existsSync(TOB_CACHE_DIR)) {
|
|
42
37
|
return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
|
|
43
|
-
} catch (_e) {
|
|
44
|
-
return []
|
|
45
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
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return []
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
export function createConfigHandler(
|
|
@@ -83,6 +93,7 @@ export function createConfigHandler(
|
|
|
83
93
|
argus_forge_fuzz: "allow",
|
|
84
94
|
argus_analyze_contract: "allow",
|
|
85
95
|
argus_check_patterns: "allow",
|
|
96
|
+
argus_skill_load: "allow",
|
|
86
97
|
skill: "allow",
|
|
87
98
|
},
|
|
88
99
|
},
|
|
@@ -94,6 +105,7 @@ export function createConfigHandler(
|
|
|
94
105
|
permission: {
|
|
95
106
|
argus_solodit_search: "allow",
|
|
96
107
|
argus_check_patterns: "allow",
|
|
108
|
+
argus_skill_load: "allow",
|
|
97
109
|
skill: "allow",
|
|
98
110
|
},
|
|
99
111
|
},
|
|
@@ -104,6 +116,7 @@ export function createConfigHandler(
|
|
|
104
116
|
prompt: SCRIBE_PROMPT,
|
|
105
117
|
permission: {
|
|
106
118
|
argus_generate_report: "allow",
|
|
119
|
+
argus_skill_load: "allow",
|
|
107
120
|
skill: "allow",
|
|
108
121
|
},
|
|
109
122
|
},
|