solidity-argus 0.2.0 → 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 +93 -37
- package/package.json +33 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- 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/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -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 +2 -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 +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- 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 +1 -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 +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -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 +2 -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 +2 -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 +24 -7
- package/src/agents/pythia-prompt.ts +3 -4
- package/src/agents/scribe-prompt.ts +7 -2
- package/src/agents/sentinel-prompt.ts +32 -3
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- 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 +4 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- 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 +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- 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/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/tool-tracking-hook.ts +104 -50
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -36
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- 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 +16 -3
- 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 +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- 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 +185 -190
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +268 -200
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +216 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +103 -74
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- 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 = "2"
|
|
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,107 +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"
|
|
42
|
+
const hasSupportedVersion = value.version === "1" || value.version === "2"
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
|
-
typeof value.savedAt === "number" &&
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
96
|
}
|
|
50
97
|
|
|
51
98
|
export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
52
|
-
const logger = createLogger()
|
|
53
|
-
const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME)
|
|
54
|
-
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
|
|
55
102
|
|
|
56
103
|
async function load(): Promise<AuditState | null> {
|
|
57
104
|
try {
|
|
58
|
-
const file = Bun.file(stateFilePath)
|
|
105
|
+
const file = Bun.file(stateFilePath)
|
|
59
106
|
if (!(await file.exists())) {
|
|
60
|
-
return null
|
|
107
|
+
return null
|
|
61
108
|
}
|
|
62
109
|
|
|
63
|
-
const content = await file.text()
|
|
110
|
+
const content = await file.text()
|
|
64
111
|
if (!content.trim()) {
|
|
65
|
-
return null
|
|
112
|
+
return null
|
|
66
113
|
}
|
|
67
114
|
|
|
68
|
-
const parsed: unknown = JSON.parse(content)
|
|
115
|
+
const parsed: unknown = JSON.parse(content)
|
|
69
116
|
if (!isPersistentAuditState(parsed)) {
|
|
70
|
-
logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
|
|
71
|
-
return null
|
|
117
|
+
logger.warn("Persistent audit state is invalid, ignoring", stateFilePath)
|
|
118
|
+
return null
|
|
72
119
|
}
|
|
73
120
|
|
|
74
|
-
const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed
|
|
121
|
+
const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed
|
|
75
122
|
|
|
76
123
|
if (version === "1") {
|
|
77
124
|
if (!state.soloditResults) {
|
|
78
|
-
state.soloditResults = []
|
|
125
|
+
state.soloditResults = []
|
|
79
126
|
}
|
|
80
127
|
if (!state.fuzzCounterexamples) {
|
|
81
|
-
state.fuzzCounterexamples = []
|
|
128
|
+
state.fuzzCounterexamples = []
|
|
82
129
|
}
|
|
83
130
|
}
|
|
84
131
|
|
|
85
|
-
currentState = state
|
|
86
|
-
return currentState
|
|
87
|
-
} catch (
|
|
88
|
-
|
|
132
|
+
currentState = state
|
|
133
|
+
return currentState
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.warn("Failed to load persisted audit state", err)
|
|
136
|
+
return null
|
|
89
137
|
}
|
|
90
138
|
}
|
|
91
139
|
|
|
92
|
-
let saveInFlight = false
|
|
140
|
+
let saveInFlight = false
|
|
93
141
|
|
|
94
142
|
async function save(state: AuditState): Promise<void> {
|
|
95
|
-
currentState = state
|
|
143
|
+
currentState = state
|
|
96
144
|
|
|
97
|
-
if (saveInFlight) return
|
|
98
|
-
saveInFlight = true
|
|
145
|
+
if (saveInFlight) return
|
|
146
|
+
saveInFlight = true
|
|
99
147
|
|
|
100
148
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
169
|
} finally {
|
|
115
|
-
saveInFlight = false
|
|
170
|
+
saveInFlight = false
|
|
116
171
|
}
|
|
117
172
|
}
|
|
118
173
|
|
|
119
174
|
function get(): AuditState {
|
|
120
|
-
return currentState
|
|
175
|
+
return currentState
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
async function update(patch: Partial<AuditState>): Promise<void> {
|
|
124
179
|
currentState = {
|
|
125
180
|
...currentState,
|
|
126
181
|
...patch,
|
|
127
|
-
}
|
|
182
|
+
}
|
|
128
183
|
|
|
129
|
-
await save(currentState)
|
|
184
|
+
await save(currentState)
|
|
130
185
|
}
|
|
131
186
|
|
|
132
187
|
async function reset(): Promise<void> {
|
|
133
|
-
currentState = createAuditState(projectDir).state
|
|
134
|
-
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)
|
|
135
217
|
}
|
|
136
218
|
|
|
137
219
|
return {
|
|
@@ -140,5 +222,6 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
140
222
|
get,
|
|
141
223
|
update,
|
|
142
224
|
reset,
|
|
143
|
-
|
|
225
|
+
archive,
|
|
226
|
+
}
|
|
144
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
|
+
}
|
|
@@ -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,14 +96,18 @@ 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",
|
|
96
111
|
argus_skill_load: "allow",
|
|
97
112
|
skill: "allow",
|
|
98
113
|
},
|
|
@@ -100,6 +115,7 @@ export function createConfigHandler(
|
|
|
100
115
|
pythia: {
|
|
101
116
|
mode: "subagent",
|
|
102
117
|
model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
|
|
118
|
+
steps: argusConfig.agents?.pythia?.steps ?? DEFAULT_STEPS,
|
|
103
119
|
description: "Vulnerability researcher",
|
|
104
120
|
prompt: PYTHIA_PROMPT,
|
|
105
121
|
permission: {
|
|
@@ -112,6 +128,7 @@ export function createConfigHandler(
|
|
|
112
128
|
scribe: {
|
|
113
129
|
mode: "subagent",
|
|
114
130
|
model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
|
|
131
|
+
steps: argusConfig.agents?.scribe?.steps ?? DEFAULT_STEPS,
|
|
115
132
|
description: "Audit report writer",
|
|
116
133
|
prompt: SCRIBE_PROMPT,
|
|
117
134
|
permission: {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const ARGUS_BUDGET = 2000
|
|
12
12
|
const SUBAGENT_BUDGET = 1000
|
|
13
|
-
const PRESSURE_THRESHOLD = 0.
|
|
13
|
+
const PRESSURE_THRESHOLD = 0.7
|
|
14
14
|
const PRESSURE_REDUCTION = 0.5
|
|
15
15
|
|
|
16
16
|
const ARGUS_AGENTS = new Set(["argus"])
|
|
@@ -23,10 +23,7 @@ const SUBAGENTS = new Set(["sentinel", "pythia", "scribe"])
|
|
|
23
23
|
* @param contextPressure - Current context usage ratio (0.0–1.0), from ContextMonitor
|
|
24
24
|
* @returns Token budget in tokens. 0 for non-Argus agents.
|
|
25
25
|
*/
|
|
26
|
-
export function getTokenBudgetForAgent(
|
|
27
|
-
agent: string,
|
|
28
|
-
contextPressure: number = 0,
|
|
29
|
-
): number {
|
|
26
|
+
export function getTokenBudgetForAgent(agent: string, contextPressure: number = 0): number {
|
|
30
27
|
let budget: number
|
|
31
28
|
|
|
32
29
|
if (ARGUS_AGENTS.has(agent)) {
|
package/src/hooks/event-hook.ts
CHANGED
|
@@ -1,33 +1,45 @@
|
|
|
1
|
-
import type { AuditState } from "../state/types"
|
|
2
|
-
import { createAuditState } from "../state/audit-state"
|
|
3
1
|
import { createLogger } from "../shared/logger"
|
|
2
|
+
import { createAuditState } from "../state/audit-state"
|
|
3
|
+
import type { AuditState } from "../state/types"
|
|
4
|
+
|
|
5
|
+
export type AuditEventType =
|
|
6
|
+
| "session.created"
|
|
7
|
+
| "session.idle"
|
|
8
|
+
| "session.error"
|
|
9
|
+
| "session.deleted"
|
|
10
|
+
| "audit.phase-changed"
|
|
11
|
+
| "audit.finding-added"
|
|
12
|
+
| "audit.complete"
|
|
4
13
|
|
|
5
14
|
export type EventHookFn = (input: {
|
|
6
|
-
event: { type: string; sessionId?: string }
|
|
15
|
+
event: { type: string; sessionId?: string; properties?: Record<string, unknown> }
|
|
7
16
|
}) => Promise<void>
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export function createEventHook(
|
|
18
|
+
export type EventSubHandler = (event: {
|
|
19
|
+
type: string
|
|
20
|
+
sessionId?: string
|
|
21
|
+
auditState: AuditState | null
|
|
22
|
+
setAuditState: (state: AuditState | null) => void
|
|
23
|
+
}) => Promise<void>
|
|
24
|
+
|
|
25
|
+
export function createEventHook(
|
|
26
|
+
projectDir?: string,
|
|
27
|
+
subHandlers: EventSubHandler[] = [],
|
|
28
|
+
): {
|
|
17
29
|
hook: EventHookFn
|
|
18
30
|
getAuditState: () => AuditState | null
|
|
19
31
|
setAuditState: (state: AuditState | null) => void
|
|
20
32
|
} {
|
|
33
|
+
const logger = createLogger()
|
|
21
34
|
let currentAuditState: AuditState | null = null
|
|
22
35
|
|
|
23
36
|
const getAuditState = (): AuditState | null => currentAuditState
|
|
24
|
-
|
|
25
37
|
const setAuditState = (state: AuditState | null): void => {
|
|
26
38
|
currentAuditState = state
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
const hook: EventHookFn = async (input): Promise<void> => {
|
|
30
|
-
const { type } = input.event
|
|
42
|
+
const { type, sessionId } = input.event
|
|
31
43
|
|
|
32
44
|
switch (type) {
|
|
33
45
|
case "session.created": {
|
|
@@ -38,23 +50,23 @@ export function createEventHook(projectDir?: string): {
|
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
case "session.idle": {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
if (currentAuditState) {
|
|
54
|
+
logger.debug(
|
|
55
|
+
`Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
break
|
|
59
|
+
}
|
|
48
60
|
|
|
49
61
|
case "session.error": {
|
|
50
62
|
if (currentAuditState) {
|
|
51
|
-
|
|
63
|
+
logger.error(
|
|
52
64
|
`Session error — state snapshot: ${JSON.stringify({
|
|
53
65
|
sessionId: currentAuditState.sessionId,
|
|
54
66
|
phase: currentAuditState.currentPhase,
|
|
55
67
|
findingsCount: currentAuditState.findings.length,
|
|
56
68
|
contractsReviewed: currentAuditState.contractsReviewed,
|
|
57
|
-
})}
|
|
69
|
+
})}`,
|
|
58
70
|
)
|
|
59
71
|
}
|
|
60
72
|
break
|
|
@@ -65,10 +77,22 @@ export function createEventHook(projectDir?: string): {
|
|
|
65
77
|
break
|
|
66
78
|
}
|
|
67
79
|
|
|
68
|
-
// Unknown events: no-op — never throw
|
|
69
80
|
default:
|
|
70
81
|
break
|
|
71
82
|
}
|
|
83
|
+
|
|
84
|
+
for (const handler of subHandlers) {
|
|
85
|
+
try {
|
|
86
|
+
await handler({
|
|
87
|
+
type,
|
|
88
|
+
sessionId,
|
|
89
|
+
auditState: currentAuditState,
|
|
90
|
+
setAuditState,
|
|
91
|
+
})
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
return { hook, getAuditState, setAuditState }
|
package/src/hooks/hook-system.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { HookName } from "./types"
|
|
1
|
+
import type { HookName } from "./types"
|
|
2
2
|
|
|
3
3
|
export function createHookGuard(disabledHooks: string[]) {
|
|
4
|
-
const disabledSet = new Set(disabledHooks)
|
|
4
|
+
const disabledSet = new Set(disabledHooks)
|
|
5
5
|
|
|
6
6
|
return function isHookEnabled(name: HookName): boolean {
|
|
7
|
-
return !disabledSet.has(name)
|
|
8
|
-
}
|
|
7
|
+
return !disabledSet.has(name)
|
|
8
|
+
}
|
|
9
9
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export
|
|
5
|
-
export type {
|
|
1
|
+
export type { AuditEventType, EventHookFn, EventSubHandler } from "./event-hook"
|
|
2
|
+
export { createEventHook } from "./event-hook"
|
|
3
|
+
export { createHookGuard } from "./hook-system"
|
|
4
|
+
export { safeCreateHook } from "./safe-create-hook"
|
|
5
|
+
export type { HookName } from "./types"
|