solidity-argus 0.2.0 → 0.3.2
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 +34 -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 +34 -7
- package/src/agents/pythia-prompt.ts +13 -4
- package/src/agents/scribe-prompt.ts +20 -2
- package/src/agents/sentinel-prompt.ts +45 -5
- 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 +6 -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/system-prompt-hook.ts +18 -1
- package/src/hooks/tool-tracking-hook.ts +110 -51
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +24 -37
- 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 +203 -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 +142 -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 +201 -158
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +203 -191
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/pattern-schema.ts +3 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +305 -206
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +235 -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 +175 -75
- 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
|
@@ -10,17 +10,17 @@ const TOOL_FALLBACKS: Record<string, ToolFallbackEntry> = {
|
|
|
10
10
|
slither: {
|
|
11
11
|
install: "pip install slither-analyzer",
|
|
12
12
|
fallback:
|
|
13
|
-
|
|
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
14
|
},
|
|
15
15
|
forge: {
|
|
16
16
|
install: "curl -L https://foundry.paradigm.xyz | bash && foundryup",
|
|
17
17
|
fallback:
|
|
18
|
-
|
|
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
19
|
},
|
|
20
20
|
solodit: {
|
|
21
21
|
install: "",
|
|
22
22
|
fallback:
|
|
23
|
-
|
|
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
24
|
},
|
|
25
25
|
scvd: {
|
|
26
26
|
install: "",
|
|
@@ -54,6 +54,7 @@ function resolveToolBase(tool: string): string {
|
|
|
54
54
|
|
|
55
55
|
export function createToolErrorRecoveryHandler(
|
|
56
56
|
getAuditState?: () => AuditState | null,
|
|
57
|
+
updateAuditState?: (patch: Partial<AuditState>) => Promise<void>,
|
|
57
58
|
) {
|
|
58
59
|
const logger = createLogger()
|
|
59
60
|
|
|
@@ -80,12 +81,16 @@ export function createToolErrorRecoveryHandler(
|
|
|
80
81
|
|
|
81
82
|
const unavailable = isToolUnavailable(lowerResult)
|
|
82
83
|
|
|
83
|
-
if (unavailable && getAuditState) {
|
|
84
|
+
if (unavailable && getAuditState && updateAuditState) {
|
|
84
85
|
const state = getAuditState()
|
|
85
86
|
if (state) {
|
|
86
|
-
state.unavailableTools
|
|
87
|
-
if (!
|
|
88
|
-
|
|
87
|
+
const existing = state.unavailableTools ?? []
|
|
88
|
+
if (!existing.includes(toolBase)) {
|
|
89
|
+
void updateAuditState({
|
|
90
|
+
unavailableTools: [...existing, toolBase],
|
|
91
|
+
}).catch((error: unknown) => {
|
|
92
|
+
logger.warn(`Failed to persist unavailable tool state for ${toolBase}`, error)
|
|
93
|
+
})
|
|
89
94
|
logger.info(`Recorded ${toolBase} as unavailable — fallback activated`)
|
|
90
95
|
}
|
|
91
96
|
}
|
package/src/features/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
1
|
+
export * from "./audit-enforcer"
|
|
2
|
+
export * from "./background-agent"
|
|
3
|
+
export * from "./context-monitor"
|
|
4
|
+
export * from "./error-recovery"
|
|
5
|
+
export * from "./persistent-state"
|
|
@@ -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)) {
|