solidity-argus 0.1.8 → 0.3.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/AGENTS.md +3 -3
- package/README.md +229 -13
- package/package.json +37 -8
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +72 -6
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -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 +9 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +9 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -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 +27 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +27 -10
- package/src/agents/pythia-prompt.ts +7 -8
- package/src/agents/scribe-prompt.ts +10 -5
- package/src/agents/sentinel-prompt.ts +36 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +29 -22
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +303 -23
- package/src/cli/commands/init.ts +8 -6
- package/src/cli/commands/install.ts +10 -8
- package/src/cli/commands/lint-skills.ts +118 -0
- package/src/cli/index.ts +5 -5
- package/src/cli/tui-prompts.ts +4 -2
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +225 -29
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +14 -8
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +79 -19
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +158 -52
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +31 -11
- package/src/hooks/context-budget.ts +42 -0
- package/src/hooks/event-hook.ts +48 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +19 -21
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +9 -11
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +162 -29
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -13
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +103 -83
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +110 -62
- package/src/knowledge/scvd-sync.ts +223 -47
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +19 -8
- package/src/shared/binary-utils.ts +44 -34
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +91 -17
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +237 -0
- package/src/skills/skill-schema.ts +99 -0
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +10 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +96 -44
- package/src/tools/argus-skill-load-tool.ts +78 -0
- package/src/tools/contract-analyzer-tool.ts +60 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +206 -167
- package/src/tools/pattern-loader.ts +77 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +333 -142
- package/src/tools/slither-tool.ts +300 -210
- package/src/tools/solodit-search-tool.ts +255 -80
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +118 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +175 -86
- package/src/utils/solidity-parser.ts +112 -67
- package/src/utils/solodit-health.ts +29 -0
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { mkdir, rename } from "node:fs/promises"
|
|
2
|
-
import { dirname, join } from "node:path"
|
|
3
|
-
import type { AuditStateManager } from "../../managers/types"
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
1
|
+
import { mkdir, rename } from "node:fs/promises"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import type { AuditStateManager } from "../../managers/types"
|
|
4
|
+
import { createLogger } from "../../shared/logger"
|
|
5
|
+
import { createAuditState } from "../../state/audit-state"
|
|
6
|
+
import type { AuditState, PersistentAuditState } from "../../state/types"
|
|
7
7
|
|
|
8
|
-
const STATE_FILE_DIR = ".opencode"
|
|
9
|
-
const STATE_FILE_NAME = "argus-state.json"
|
|
10
|
-
const STATE_VERSION = "
|
|
8
|
+
const STATE_FILE_DIR = ".opencode"
|
|
9
|
+
const STATE_FILE_NAME = "argus-state.json"
|
|
10
|
+
const STATE_VERSION = "2"
|
|
11
11
|
|
|
12
12
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
13
|
-
return typeof value === "object" && value !== null
|
|
13
|
+
return typeof value === "object" && value !== null
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function isStringArray(value: unknown): value is string[] {
|
|
17
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
17
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function isAuditState(value: unknown): value is AuditState {
|
|
21
21
|
if (!isObject(value)) {
|
|
22
|
-
return false
|
|
22
|
+
return false
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
return (
|
|
@@ -31,84 +31,189 @@ function isAuditState(value: unknown): value is AuditState {
|
|
|
31
31
|
typeof value.currentPhase === "string" &&
|
|
32
32
|
isStringArray(value.scope) &&
|
|
33
33
|
typeof value.startTime === "number"
|
|
34
|
-
)
|
|
34
|
+
)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function isPersistentAuditState(value: unknown): value is PersistentAuditState {
|
|
38
38
|
if (!isAuditState(value) || !isObject(value)) {
|
|
39
|
-
return false
|
|
39
|
+
return false
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const hasSupportedVersion = value.version === "1" || value.version === "2"
|
|
43
|
+
|
|
42
44
|
return (
|
|
43
|
-
typeof value.savedAt === "number" &&
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
typeof value.savedAt === "number" && hasSupportedVersion && typeof value.filePath === "string"
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createDebouncedSave(
|
|
50
|
+
saveState: (state: AuditState) => Promise<void>,
|
|
51
|
+
delayMs = 5_000,
|
|
52
|
+
): {
|
|
53
|
+
save: (state: AuditState) => void
|
|
54
|
+
flush: () => Promise<void>
|
|
55
|
+
} {
|
|
56
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
57
|
+
let pendingState: AuditState | null = null
|
|
58
|
+
|
|
59
|
+
async function persistPendingState(): Promise<void> {
|
|
60
|
+
if (!pendingState) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stateToPersist = pendingState
|
|
65
|
+
pendingState = null
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await saveState(stateToPersist)
|
|
69
|
+
} catch {
|
|
70
|
+
createLogger().debug("Debounced state persistence failed")
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
save(state: AuditState): void {
|
|
76
|
+
pendingState = state
|
|
77
|
+
|
|
78
|
+
if (timer) {
|
|
79
|
+
clearTimeout(timer)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
timer = setTimeout(() => {
|
|
83
|
+
timer = null
|
|
84
|
+
void persistPendingState()
|
|
85
|
+
}, delayMs)
|
|
86
|
+
},
|
|
87
|
+
async flush(): Promise<void> {
|
|
88
|
+
if (timer) {
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
timer = null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await persistPendingState()
|
|
94
|
+
},
|
|
95
|
+
}
|
|
47
96
|
}
|
|
48
97
|
|
|
49
98
|
export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
50
|
-
const logger = createLogger()
|
|
51
|
-
const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME)
|
|
52
|
-
let currentState: AuditState = createAuditState(projectDir).state
|
|
99
|
+
const logger = createLogger()
|
|
100
|
+
const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME)
|
|
101
|
+
let currentState: AuditState = createAuditState(projectDir).state
|
|
53
102
|
|
|
54
103
|
async function load(): Promise<AuditState | null> {
|
|
55
104
|
try {
|
|
56
|
-
const file = Bun.file(stateFilePath)
|
|
105
|
+
const file = Bun.file(stateFilePath)
|
|
57
106
|
if (!(await file.exists())) {
|
|
58
|
-
return null
|
|
107
|
+
return null
|
|
59
108
|
}
|
|
60
109
|
|
|
61
|
-
const content = await file.text()
|
|
110
|
+
const content = await file.text()
|
|
62
111
|
if (!content.trim()) {
|
|
63
|
-
return null
|
|
112
|
+
return null
|
|
64
113
|
}
|
|
65
114
|
|
|
66
|
-
const parsed: unknown = JSON.parse(content)
|
|
115
|
+
const parsed: unknown = JSON.parse(content)
|
|
67
116
|
if (!isPersistentAuditState(parsed)) {
|
|
68
|
-
logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
|
|
69
|
-
return null
|
|
117
|
+
logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
|
|
118
|
+
return null
|
|
70
119
|
}
|
|
71
120
|
|
|
72
|
-
const { savedAt: _savedAt, version
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
121
|
+
const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed
|
|
122
|
+
|
|
123
|
+
if (version === "1") {
|
|
124
|
+
if (!state.soloditResults) {
|
|
125
|
+
state.soloditResults = []
|
|
126
|
+
}
|
|
127
|
+
if (!state.fuzzCounterexamples) {
|
|
128
|
+
state.fuzzCounterexamples = []
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
currentState = state
|
|
133
|
+
return currentState
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.warn("Failed to load persisted audit state", err)
|
|
136
|
+
return null
|
|
77
137
|
}
|
|
78
138
|
}
|
|
79
139
|
|
|
140
|
+
let saveInFlight = false
|
|
141
|
+
|
|
80
142
|
async function save(state: AuditState): Promise<void> {
|
|
81
|
-
currentState = state
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
143
|
+
currentState = state
|
|
144
|
+
|
|
145
|
+
if (saveInFlight) return
|
|
146
|
+
saveInFlight = true
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
while (true) {
|
|
150
|
+
const stateToSave = currentState
|
|
151
|
+
|
|
152
|
+
const persistentState: PersistentAuditState = {
|
|
153
|
+
...stateToSave,
|
|
154
|
+
savedAt: Date.now(),
|
|
155
|
+
version: STATE_VERSION,
|
|
156
|
+
filePath: stateFilePath,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`
|
|
160
|
+
await mkdir(dirname(stateFilePath), { recursive: true })
|
|
161
|
+
await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`)
|
|
162
|
+
await rename(tempFilePath, stateFilePath)
|
|
163
|
+
|
|
164
|
+
if (currentState === stateToSave) break
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
logger.warn("Failed to persist audit state", err)
|
|
168
|
+
throw err
|
|
169
|
+
} finally {
|
|
170
|
+
saveInFlight = false
|
|
171
|
+
}
|
|
94
172
|
}
|
|
95
173
|
|
|
96
174
|
function get(): AuditState {
|
|
97
|
-
return currentState
|
|
175
|
+
return currentState
|
|
98
176
|
}
|
|
99
177
|
|
|
100
178
|
async function update(patch: Partial<AuditState>): Promise<void> {
|
|
101
179
|
currentState = {
|
|
102
180
|
...currentState,
|
|
103
181
|
...patch,
|
|
104
|
-
}
|
|
182
|
+
}
|
|
105
183
|
|
|
106
|
-
await save(currentState)
|
|
184
|
+
await save(currentState)
|
|
107
185
|
}
|
|
108
186
|
|
|
109
187
|
async function reset(): Promise<void> {
|
|
110
|
-
currentState = createAuditState(projectDir).state
|
|
111
|
-
await save(currentState)
|
|
188
|
+
currentState = createAuditState(projectDir).state
|
|
189
|
+
await save(currentState)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function archive(): Promise<void> {
|
|
193
|
+
const hasContent =
|
|
194
|
+
currentState.findings.length > 0 ||
|
|
195
|
+
currentState.toolsExecuted.length > 0 ||
|
|
196
|
+
currentState.currentPhase !== "reconnaissance"
|
|
197
|
+
|
|
198
|
+
if (hasContent) {
|
|
199
|
+
try {
|
|
200
|
+
const archivesDir = join(dirname(stateFilePath), "archives")
|
|
201
|
+
await mkdir(archivesDir, { recursive: true })
|
|
202
|
+
const archivePath = join(archivesDir, `argus-state.${Date.now()}.json`)
|
|
203
|
+
const persistentState: PersistentAuditState = {
|
|
204
|
+
...currentState,
|
|
205
|
+
savedAt: Date.now(),
|
|
206
|
+
version: STATE_VERSION,
|
|
207
|
+
filePath: archivePath,
|
|
208
|
+
}
|
|
209
|
+
await Bun.write(archivePath, `${JSON.stringify(persistentState, null, 2)}\n`)
|
|
210
|
+
} catch {
|
|
211
|
+
logger.debug("Failed to archive audit state")
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
currentState = createAuditState(projectDir).state
|
|
216
|
+
await save(currentState)
|
|
112
217
|
}
|
|
113
218
|
|
|
114
219
|
return {
|
|
@@ -117,5 +222,6 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
117
222
|
get,
|
|
118
223
|
update,
|
|
119
224
|
reset,
|
|
120
|
-
|
|
225
|
+
archive,
|
|
226
|
+
}
|
|
121
227
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { createLogger } from "../../shared/logger"
|
|
6
|
+
|
|
7
|
+
const logger = createLogger()
|
|
8
|
+
|
|
9
|
+
const CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "runs")
|
|
10
|
+
const INDEX_FILE = join(CACHE_DIR, "index.jsonl")
|
|
11
|
+
|
|
12
|
+
export type RunIndexEntry = {
|
|
13
|
+
runId: string
|
|
14
|
+
opencodeSessionId?: string
|
|
15
|
+
projectDir: string
|
|
16
|
+
statePath: string
|
|
17
|
+
journalPath: string
|
|
18
|
+
startedAt: number
|
|
19
|
+
phase: string
|
|
20
|
+
findingsCount: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let dirEnsured = false
|
|
24
|
+
|
|
25
|
+
async function ensureDir(): Promise<void> {
|
|
26
|
+
if (dirEnsured) return
|
|
27
|
+
await mkdir(CACHE_DIR, { recursive: true })
|
|
28
|
+
dirEnsured = true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function recordRun(entry: RunIndexEntry): Promise<void> {
|
|
32
|
+
try {
|
|
33
|
+
await ensureDir()
|
|
34
|
+
appendFileSync(INDEX_FILE, `${JSON.stringify(entry)}\n`)
|
|
35
|
+
} catch {
|
|
36
|
+
logger.debug("Failed to write global run index entry")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createAuditStateManager } from "./audit-state-manager"
|
|
1
|
+
export { createAuditStateManager } from "./audit-state-manager"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { createLogger } from "../../shared/logger"
|
|
4
|
+
|
|
5
|
+
const logger = createLogger()
|
|
6
|
+
|
|
7
|
+
const JOURNAL_DIR = ".opencode"
|
|
8
|
+
const JOURNAL_FILE = "argus-journal.jsonl"
|
|
9
|
+
|
|
10
|
+
export type JournalEvent =
|
|
11
|
+
| { type: "session.created"; sessionId?: string; timestamp: number }
|
|
12
|
+
| {
|
|
13
|
+
type: "session.idle"
|
|
14
|
+
timestamp: number
|
|
15
|
+
findingsCount: number
|
|
16
|
+
toolsExecutedCount: number
|
|
17
|
+
}
|
|
18
|
+
| { type: "session.deleted"; timestamp: number; archived: boolean }
|
|
19
|
+
| {
|
|
20
|
+
type: "tool.executed"
|
|
21
|
+
tool: string
|
|
22
|
+
timestamp: number
|
|
23
|
+
findingsCount: number
|
|
24
|
+
}
|
|
25
|
+
| { type: "state.saved"; timestamp: number; success: boolean }
|
|
26
|
+
| {
|
|
27
|
+
type: "state.loaded"
|
|
28
|
+
timestamp: number
|
|
29
|
+
success: boolean
|
|
30
|
+
findingsCount: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createRunJournal(projectDir: string): {
|
|
34
|
+
log(event: JournalEvent): void
|
|
35
|
+
close(): Promise<void>
|
|
36
|
+
getPath(): string
|
|
37
|
+
} {
|
|
38
|
+
const journalPath = join(projectDir, JOURNAL_DIR, JOURNAL_FILE)
|
|
39
|
+
let ensureDirPromise: Promise<void> | null = null
|
|
40
|
+
const pendingWrites = new Set<Promise<void>>()
|
|
41
|
+
|
|
42
|
+
function ensureDirectory(): Promise<void> {
|
|
43
|
+
if (!ensureDirPromise) {
|
|
44
|
+
ensureDirPromise = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
await mkdir(dirname(journalPath), { recursive: true })
|
|
47
|
+
} catch {
|
|
48
|
+
logger.debug("Failed to create run journal directory")
|
|
49
|
+
}
|
|
50
|
+
})()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return ensureDirPromise
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function trackWrite(writePromise: Promise<void>): void {
|
|
57
|
+
pendingWrites.add(writePromise)
|
|
58
|
+
void writePromise.finally(() => {
|
|
59
|
+
pendingWrites.delete(writePromise)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function log(event: JournalEvent): void {
|
|
64
|
+
const line = `${JSON.stringify(event)}\n`
|
|
65
|
+
|
|
66
|
+
const writePromise = ensureDirectory()
|
|
67
|
+
.then(async () => {
|
|
68
|
+
await appendFile(journalPath, line, "utf8")
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
logger.debug("Failed to append run journal event")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
trackWrite(writePromise)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function close(): Promise<void> {
|
|
78
|
+
await Promise.allSettled(Array.from(pendingWrites))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
log,
|
|
83
|
+
close,
|
|
84
|
+
getPath: () => journalPath,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -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,17 +1,19 @@
|
|
|
1
|
-
import { resolve, join } from "node:path"
|
|
2
1
|
import { existsSync, readdirSync } from "node:fs"
|
|
3
2
|
import { homedir } from "node:os"
|
|
3
|
+
import { join, resolve } from "node:path"
|
|
4
4
|
import type { Config } from "@opencode-ai/sdk/v2"
|
|
5
|
-
import type { ArgusConfig } from "../config/types"
|
|
6
|
-
import { DEFAULT_MODELS } from "../constants/defaults"
|
|
7
|
-
import { createKnowledgeSyncHook } from "./knowledge-sync-hook"
|
|
8
5
|
import { ARGUS_PROMPT } from "../agents/argus-prompt"
|
|
9
|
-
import { SENTINEL_PROMPT } from "../agents/sentinel-prompt"
|
|
10
6
|
import { PYTHIA_PROMPT } from "../agents/pythia-prompt"
|
|
11
7
|
import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
|
|
8
|
+
import { SENTINEL_PROMPT } from "../agents/sentinel-prompt"
|
|
9
|
+
import type { ArgusConfig } from "../config/types"
|
|
10
|
+
import { DEFAULT_MODELS, DEFAULT_STEPS } from "../constants/defaults"
|
|
11
|
+
import { createLogger } from "../shared/logger"
|
|
12
|
+
import { createKnowledgeSyncHook } from "./knowledge-sync-hook"
|
|
12
13
|
|
|
13
14
|
const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
|
|
14
15
|
const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
|
|
16
|
+
const TOB_BRANCH = "main"
|
|
15
17
|
let tobCloneInFlight = false
|
|
16
18
|
|
|
17
19
|
function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
|
|
@@ -40,24 +42,32 @@ function ensureTrailOfBitsSkills(): string[] {
|
|
|
40
42
|
if (!tobCloneInFlight) {
|
|
41
43
|
tobCloneInFlight = true
|
|
42
44
|
const cloneProcess = Bun.spawn(
|
|
43
|
-
["git", "clone", "--depth", "1", TOB_REPO_URL, TOB_CACHE_DIR],
|
|
45
|
+
["git", "clone", "--depth", "1", "--branch", TOB_BRANCH, TOB_REPO_URL, TOB_CACHE_DIR],
|
|
44
46
|
{
|
|
45
47
|
stdin: "ignore",
|
|
46
48
|
stdout: "ignore",
|
|
47
49
|
stderr: "ignore",
|
|
50
|
+
signal: AbortSignal.timeout(60_000),
|
|
48
51
|
},
|
|
49
52
|
)
|
|
50
|
-
cloneProcess.exited
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
cloneProcess.exited
|
|
54
|
+
.then((code) => {
|
|
55
|
+
if (code !== 0) {
|
|
56
|
+
const logger = createLogger()
|
|
57
|
+
logger.warn(`Trail of Bits skills clone failed with exit code ${code}`)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
tobCloneInFlight = false
|
|
62
|
+
})
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
return []
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
export function createConfigHandler(
|
|
59
69
|
argusConfig: ArgusConfig,
|
|
60
|
-
projectDir: string = process.cwd()
|
|
70
|
+
projectDir: string = process.cwd(),
|
|
61
71
|
): (config: Config) => Promise<void> {
|
|
62
72
|
const triggerKnowledgeSync = createKnowledgeSyncHook(argusConfig)
|
|
63
73
|
|
|
@@ -67,6 +77,7 @@ export function createConfigHandler(
|
|
|
67
77
|
argus: {
|
|
68
78
|
mode: "primary",
|
|
69
79
|
model: argusConfig.agents?.argus?.model ?? DEFAULT_MODELS.argus,
|
|
80
|
+
steps: argusConfig.agents?.argus?.steps ?? DEFAULT_STEPS,
|
|
70
81
|
description: "Solidity security auditor — the All-Seeing Guardian",
|
|
71
82
|
prompt: ARGUS_PROMPT,
|
|
72
83
|
tools: {
|
|
@@ -85,35 +96,44 @@ export function createConfigHandler(
|
|
|
85
96
|
sentinel: {
|
|
86
97
|
mode: "subagent",
|
|
87
98
|
model: argusConfig.agents?.sentinel?.model ?? DEFAULT_MODELS.sentinel,
|
|
99
|
+
steps: argusConfig.agents?.sentinel?.steps ?? DEFAULT_STEPS,
|
|
88
100
|
description: "Static analysis and testing specialist",
|
|
89
101
|
prompt: SENTINEL_PROMPT,
|
|
90
102
|
permission: {
|
|
91
103
|
argus_slither_analyze: "allow",
|
|
92
104
|
argus_forge_test: "allow",
|
|
105
|
+
argus_gas_analysis: "allow",
|
|
93
106
|
argus_forge_fuzz: "allow",
|
|
94
107
|
argus_analyze_contract: "allow",
|
|
95
108
|
argus_check_patterns: "allow",
|
|
109
|
+
argus_proxy_detection: "allow",
|
|
110
|
+
argus_forge_coverage: "allow",
|
|
111
|
+
argus_skill_load: "allow",
|
|
96
112
|
skill: "allow",
|
|
97
113
|
},
|
|
98
114
|
},
|
|
99
115
|
pythia: {
|
|
100
116
|
mode: "subagent",
|
|
101
117
|
model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
|
|
118
|
+
steps: argusConfig.agents?.pythia?.steps ?? DEFAULT_STEPS,
|
|
102
119
|
description: "Vulnerability researcher",
|
|
103
120
|
prompt: PYTHIA_PROMPT,
|
|
104
121
|
permission: {
|
|
105
122
|
argus_solodit_search: "allow",
|
|
106
123
|
argus_check_patterns: "allow",
|
|
124
|
+
argus_skill_load: "allow",
|
|
107
125
|
skill: "allow",
|
|
108
126
|
},
|
|
109
127
|
},
|
|
110
128
|
scribe: {
|
|
111
129
|
mode: "subagent",
|
|
112
130
|
model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
|
|
131
|
+
steps: argusConfig.agents?.scribe?.steps ?? DEFAULT_STEPS,
|
|
113
132
|
description: "Audit report writer",
|
|
114
133
|
prompt: SCRIBE_PROMPT,
|
|
115
134
|
permission: {
|
|
116
135
|
argus_generate_report: "allow",
|
|
136
|
+
argus_skill_load: "allow",
|
|
117
137
|
skill: "allow",
|
|
118
138
|
},
|
|
119
139
|
},
|