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
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"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import os from "node:os"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import { ScvdClient } from "../knowledge/scvd-client"
|
|
4
|
-
import { syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
|
|
5
3
|
import type { ArgusConfig } from "../config/types"
|
|
4
|
+
import { ScvdClient } from "../knowledge/scvd-client"
|
|
5
|
+
import { type SyncResult, syncIncremental } from "../knowledge/scvd-sync"
|
|
6
6
|
import { createLogger } from "../shared/logger"
|
|
7
7
|
|
|
8
8
|
export type KnowledgeSyncDependencies = {
|
|
@@ -19,14 +19,14 @@ function defaultDependencies(): Required<KnowledgeSyncDependencies> {
|
|
|
19
19
|
syncIncrementalFn: async (client: unknown, indexPath: string) =>
|
|
20
20
|
syncIncremental(client as ScvdClient, indexPath),
|
|
21
21
|
log: (message: string) => {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
createLogger().info(message)
|
|
23
|
+
},
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function createKnowledgeSyncHook(
|
|
28
28
|
argusConfig: ArgusConfig,
|
|
29
|
-
deps: KnowledgeSyncDependencies = {}
|
|
29
|
+
deps: KnowledgeSyncDependencies = {},
|
|
30
30
|
): () => void {
|
|
31
31
|
const dependencies = { ...defaultDependencies(), ...deps }
|
|
32
32
|
|
|
@@ -36,23 +36,20 @@ export function createKnowledgeSyncHook(
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
|
|
39
|
-
const indexPath = path.join(
|
|
40
|
-
os.homedir(),
|
|
41
|
-
".cache",
|
|
42
|
-
"solidity-argus",
|
|
43
|
-
"scvd-index.json"
|
|
44
|
-
)
|
|
39
|
+
const indexPath = path.join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
|
|
45
40
|
|
|
46
41
|
Promise.resolve().then(async () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
42
|
+
try {
|
|
43
|
+
const client = dependencies.createClient(apiUrl)
|
|
44
|
+
const result = await dependencies.syncIncrementalFn(client, indexPath)
|
|
45
|
+
if (result.newFindings > 0) {
|
|
46
|
+
dependencies.log(
|
|
47
|
+
`[argus] SCVD index updated: ${result.newFindings} new findings (total: ${result.totalIndexed})`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
createLogger().debug("Knowledge sync failed during auto-sync")
|
|
52
|
+
}
|
|
53
|
+
})
|
|
57
54
|
}
|
|
58
55
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ProjectConfig } from "../utils/project-detector"
|
|
2
|
-
import type { DependencyRisk } from "../utils/dependency-scanner"
|
|
3
1
|
import type { AuditArtifact } from "../utils/audit-artifact-detector"
|
|
2
|
+
import type { DependencyRisk } from "../utils/dependency-scanner"
|
|
3
|
+
import type { ProjectConfig } from "../utils/project-detector"
|
|
4
4
|
|
|
5
5
|
export interface ReconContext {
|
|
6
6
|
projectConfig: ProjectConfig | null
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { createLogger } from "../shared/logger"
|
|
2
2
|
|
|
3
|
-
export function safeCreateHook<T>(
|
|
4
|
-
factory: () => T,
|
|
5
|
-
hookName: string
|
|
6
|
-
): T | undefined {
|
|
3
|
+
export function safeCreateHook<T>(factory: () => T, hookName: string): T | undefined {
|
|
7
4
|
try {
|
|
8
|
-
return factory()
|
|
5
|
+
return factory()
|
|
9
6
|
} catch (error) {
|
|
10
7
|
const logger = createLogger()
|
|
11
|
-
logger.error(
|
|
12
|
-
|
|
8
|
+
logger.error(
|
|
9
|
+
`Failed to create hook "${hookName}": ${error instanceof Error ? error.message : String(error)}`,
|
|
10
|
+
)
|
|
11
|
+
return undefined
|
|
13
12
|
}
|
|
14
13
|
}
|
|
@@ -3,6 +3,15 @@ import type { AuditState, FindingSeverity } from "../state/types"
|
|
|
3
3
|
const DEFAULT_TOKEN_BUDGET = 2000
|
|
4
4
|
const TOKENS_PER_CHAR = 4
|
|
5
5
|
|
|
6
|
+
const TOOL_SHORT_NAMES: Record<string, string> = {
|
|
7
|
+
argus_slither_analyze: "slither",
|
|
8
|
+
argus_forge_test: "forge-test",
|
|
9
|
+
argus_check_patterns: "patterns",
|
|
10
|
+
argus_solodit_search: "solodit",
|
|
11
|
+
argus_analyze_contract: "analyzer",
|
|
12
|
+
}
|
|
13
|
+
const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
|
|
14
|
+
|
|
6
15
|
export interface SystemPromptHookDeps {
|
|
7
16
|
getAuditState: () => AuditState | null
|
|
8
17
|
getAgentForSession: (sessionID: string) => string | undefined
|
|
@@ -52,7 +61,13 @@ export function buildDynamicContext(
|
|
|
52
61
|
severityCounts[finding.severity]++
|
|
53
62
|
}
|
|
54
63
|
|
|
64
|
+
const executedToolNames = new Set(
|
|
65
|
+
auditState.toolsExecuted.map((t) => TOOL_SHORT_NAMES[t.tool] ?? t.tool),
|
|
66
|
+
)
|
|
55
67
|
const tools = auditState.toolsExecuted.map((tool) => tool.tool).join(", ") || "none"
|
|
68
|
+
const taskStatus = KEY_TOOLS.map(
|
|
69
|
+
(t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
|
|
70
|
+
).join(" ")
|
|
56
71
|
const unavailable = auditState.unavailableTools ?? []
|
|
57
72
|
const lines: string[] = [
|
|
58
73
|
`<argus-context agent="${agent}">`,
|
|
@@ -60,6 +75,7 @@ export function buildDynamicContext(
|
|
|
60
75
|
`Contracts: ${auditState.contractsReviewed.length} reviewed`,
|
|
61
76
|
`Findings: Critical=${severityCounts.Critical} High=${severityCounts.High} Medium=${severityCounts.Medium} Low=${severityCounts.Low} Info=${severityCounts.Informational}`,
|
|
62
77
|
`Tools: ${tools}`,
|
|
78
|
+
`Tasks: ${taskStatus}`,
|
|
63
79
|
]
|
|
64
80
|
|
|
65
81
|
if (unavailable.length > 0) {
|
|
@@ -72,9 +88,10 @@ export function buildDynamicContext(
|
|
|
72
88
|
let summary = lines.join("\n")
|
|
73
89
|
|
|
74
90
|
if (estimateTokens(summary) > tokenBudget) {
|
|
91
|
+
const doneCount = KEY_TOOLS.filter((t) => executedToolNames.has(t)).length
|
|
75
92
|
summary = [
|
|
76
93
|
`<argus-context agent="${agent}">`,
|
|
77
|
-
`Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length}`,
|
|
94
|
+
`Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length} | Tasks: ${doneCount}/${KEY_TOOLS.length} done`,
|
|
78
95
|
"</argus-context>",
|
|
79
96
|
].join("\n")
|
|
80
97
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AuditState, FindingSeverity, FuzzCounterexample, SoloditResult } from "../state/types"
|
|
2
1
|
import type { FindingStore } from "../state/finding-store"
|
|
3
2
|
import { createFindingStore } from "../state/finding-store"
|
|
3
|
+
import type { AuditState, FindingSeverity, FuzzCounterexample, SoloditResult } from "../state/types"
|
|
4
4
|
|
|
5
5
|
type ToolHookInput = {
|
|
6
6
|
tool: string
|
|
@@ -8,6 +8,11 @@ type ToolHookInput = {
|
|
|
8
8
|
result: string
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
type ToolExecutionMetadata = {
|
|
12
|
+
tool: string
|
|
13
|
+
findingsCount: number
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const VALID_SEVERITIES: ReadonlySet<string> = new Set([
|
|
12
17
|
"Critical",
|
|
13
18
|
"High",
|
|
@@ -16,11 +21,7 @@ const VALID_SEVERITIES: ReadonlySet<string> = new Set([
|
|
|
16
21
|
"Informational",
|
|
17
22
|
])
|
|
18
23
|
|
|
19
|
-
const VALID_CONFIDENCES: ReadonlySet<string> = new Set([
|
|
20
|
-
"High",
|
|
21
|
-
"Medium",
|
|
22
|
-
"Low",
|
|
23
|
-
])
|
|
24
|
+
const VALID_CONFIDENCES: ReadonlySet<string> = new Set(["High", "Medium", "Low"])
|
|
24
25
|
|
|
25
26
|
function toSeverity(value: unknown): FindingSeverity {
|
|
26
27
|
if (typeof value === "string" && VALID_SEVERITIES.has(value)) {
|
|
@@ -55,10 +56,7 @@ function toRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
55
56
|
return undefined
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function processSlitherResult(
|
|
59
|
-
parsed: Record<string, unknown>,
|
|
60
|
-
store: FindingStore
|
|
61
|
-
): number {
|
|
59
|
+
function processSlitherResult(parsed: Record<string, unknown>, store: FindingStore): number {
|
|
62
60
|
const findings = parsed.findings
|
|
63
61
|
if (!Array.isArray(findings)) return 0
|
|
64
62
|
|
|
@@ -96,10 +94,7 @@ function processSlitherResult(
|
|
|
96
94
|
return count
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
function processPatternResult(
|
|
100
|
-
parsed: Record<string, unknown>,
|
|
101
|
-
store: FindingStore
|
|
102
|
-
): number {
|
|
97
|
+
function processPatternResult(parsed: Record<string, unknown>, store: FindingStore): number {
|
|
103
98
|
const sources = parsed.sources
|
|
104
99
|
if (!Array.isArray(sources)) return 0
|
|
105
100
|
|
|
@@ -145,10 +140,7 @@ function processPatternResult(
|
|
|
145
140
|
return count
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
function processContractAnalyzerResult(
|
|
149
|
-
parsed: Record<string, unknown>,
|
|
150
|
-
state: AuditState
|
|
151
|
-
): void {
|
|
143
|
+
function processContractAnalyzerResult(parsed: Record<string, unknown>, state: AuditState): void {
|
|
152
144
|
// Handle direct ContractProfile format (actual tool output)
|
|
153
145
|
if (typeof parsed.filePath === "string") {
|
|
154
146
|
if (!state.contractsReviewed.includes(parsed.filePath)) {
|
|
@@ -166,15 +158,11 @@ function processContractAnalyzerResult(
|
|
|
166
158
|
}
|
|
167
159
|
}
|
|
168
160
|
|
|
169
|
-
function processFuzzResult(
|
|
170
|
-
parsed: Record<string, unknown>,
|
|
171
|
-
state: AuditState
|
|
172
|
-
): void {
|
|
161
|
+
function processFuzzResult(parsed: Record<string, unknown>, state: AuditState): void {
|
|
173
162
|
const counterexamples = parsed.counterexamples
|
|
174
163
|
if (!Array.isArray(counterexamples) || counterexamples.length === 0) return
|
|
175
164
|
|
|
176
|
-
const totalRuns =
|
|
177
|
-
typeof parsed.totalRuns === "number" ? parsed.totalRuns : 0
|
|
165
|
+
const totalRuns = typeof parsed.totalRuns === "number" ? parsed.totalRuns : 0
|
|
178
166
|
|
|
179
167
|
state.fuzzCounterexamples ??= []
|
|
180
168
|
|
|
@@ -185,8 +173,13 @@ function processFuzzResult(
|
|
|
185
173
|
const testName = ce.testName
|
|
186
174
|
if (typeof testName !== "string") continue
|
|
187
175
|
|
|
188
|
-
const rawInputs =
|
|
189
|
-
const inputs =
|
|
176
|
+
const rawInputs = ce.inputs
|
|
177
|
+
const inputs = Array.isArray(rawInputs)
|
|
178
|
+
? rawInputs.map(String)
|
|
179
|
+
: (() => {
|
|
180
|
+
const rec = toRecord(rawInputs)
|
|
181
|
+
return rec ? Object.values(rec).map(String) : []
|
|
182
|
+
})()
|
|
190
183
|
|
|
191
184
|
const entry: FuzzCounterexample = {
|
|
192
185
|
testName,
|
|
@@ -203,26 +196,20 @@ function processFuzzResult(
|
|
|
203
196
|
}
|
|
204
197
|
}
|
|
205
198
|
|
|
206
|
-
function processSoloditResult(
|
|
207
|
-
parsed: Record<string, unknown>,
|
|
208
|
-
state: AuditState
|
|
209
|
-
): void {
|
|
199
|
+
function processSoloditResult(parsed: Record<string, unknown>, state: AuditState): void {
|
|
210
200
|
const query = typeof parsed.query === "string" ? parsed.query : ""
|
|
211
201
|
const results = Array.isArray(parsed.results) ? parsed.results : []
|
|
212
|
-
const totalFound =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
protocol: typeof r?.protocol === "string" ? r.protocol : "",
|
|
224
|
-
}
|
|
225
|
-
})
|
|
202
|
+
const totalFound = typeof parsed.totalFound === "number" ? parsed.totalFound : results.length
|
|
203
|
+
|
|
204
|
+
const topResults: SoloditResult["topResults"] = results.slice(0, 5).map((raw) => {
|
|
205
|
+
const r = toRecord(raw)
|
|
206
|
+
return {
|
|
207
|
+
title: typeof r?.title === "string" ? r.title : "",
|
|
208
|
+
severity: typeof r?.severity === "string" ? r.severity : "",
|
|
209
|
+
url: typeof r?.url === "string" ? r.url : "",
|
|
210
|
+
protocol: typeof r?.protocol === "string" ? r.protocol : "",
|
|
211
|
+
}
|
|
212
|
+
})
|
|
226
213
|
|
|
227
214
|
state.soloditResults ??= []
|
|
228
215
|
state.soloditResults.push({
|
|
@@ -246,11 +233,7 @@ function processSoloditResult(
|
|
|
246
233
|
* architecture. For accurate timing, the hook would need to fire in tool.execute.before
|
|
247
234
|
* and tool.execute.after phases separately.
|
|
248
235
|
*/
|
|
249
|
-
function recordToolExecution(
|
|
250
|
-
state: AuditState,
|
|
251
|
-
toolName: string,
|
|
252
|
-
findingsCount: number
|
|
253
|
-
): void {
|
|
236
|
+
function recordToolExecution(state: AuditState, toolName: string, findingsCount: number): void {
|
|
254
237
|
const now = Date.now()
|
|
255
238
|
state.toolsExecuted.push({
|
|
256
239
|
tool: toolName,
|
|
@@ -269,7 +252,8 @@ function recordToolExecution(
|
|
|
269
252
|
* Findings are deduplicated via the FindingStore (by check+file+lines).
|
|
270
253
|
*/
|
|
271
254
|
export function createToolTrackingHook(
|
|
272
|
-
getAuditState: () => AuditState | null
|
|
255
|
+
getAuditState: () => AuditState | null,
|
|
256
|
+
onStateChanged?: (metadata: ToolExecutionMetadata) => void,
|
|
273
257
|
): (input: ToolHookInput) => Promise<void> {
|
|
274
258
|
const storesByState = new WeakMap<AuditState, FindingStore>()
|
|
275
259
|
|
|
@@ -296,6 +280,22 @@ export function createToolTrackingHook(
|
|
|
296
280
|
|
|
297
281
|
const { state: auditState, store } = resolved
|
|
298
282
|
|
|
283
|
+
// Handle argus_skill_load first — it returns markdown, not JSON
|
|
284
|
+
if (input.tool === "argus_skill_load") {
|
|
285
|
+
// Extract skill name from markdown header: "## Argus Skill: {name} [Source: ...]"
|
|
286
|
+
const nameMatch = input.result.match(/^##\s+Argus Skill:\s+(.+?)(?:\s+\[|$)/m)
|
|
287
|
+
const skillName = nameMatch?.[1]?.trim()
|
|
288
|
+
if (skillName) {
|
|
289
|
+
auditState.skillsLoaded ??= []
|
|
290
|
+
if (!auditState.skillsLoaded.includes(skillName)) {
|
|
291
|
+
auditState.skillsLoaded.push(skillName)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
recordToolExecution(auditState, input.tool, 0)
|
|
295
|
+
onStateChanged?.({ tool: input.tool, findingsCount: 0 })
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
299
|
let parsed: unknown
|
|
300
300
|
try {
|
|
301
301
|
parsed = JSON.parse(input.result)
|
|
@@ -321,13 +321,72 @@ export function createToolTrackingHook(
|
|
|
321
321
|
case "argus_solodit_search":
|
|
322
322
|
processSoloditResult(record, auditState)
|
|
323
323
|
break
|
|
324
|
-
case "argus_forge_test":
|
|
324
|
+
case "argus_forge_test": {
|
|
325
|
+
const summary = toRecord(record.summary)
|
|
326
|
+
if (summary && typeof summary.failed === "number") {
|
|
327
|
+
findingsCount = summary.failed
|
|
328
|
+
}
|
|
325
329
|
break
|
|
330
|
+
}
|
|
326
331
|
case "argus_forge_fuzz":
|
|
327
332
|
processFuzzResult(record, auditState)
|
|
328
333
|
break
|
|
334
|
+
case "argus_generate_report": {
|
|
335
|
+
auditState.reportGenerated = true
|
|
336
|
+
break
|
|
337
|
+
}
|
|
338
|
+
case "argus_sync_knowledge": {
|
|
339
|
+
const success = record.success === true
|
|
340
|
+
auditState.knowledgeSynced = { success, timestamp: Date.now() }
|
|
341
|
+
break
|
|
342
|
+
}
|
|
343
|
+
case "argus_forge_coverage": {
|
|
344
|
+
const reportObj = toRecord(record.report)
|
|
345
|
+
const files = reportObj?.files
|
|
346
|
+
if (Array.isArray(files)) {
|
|
347
|
+
auditState.coverageReport = {
|
|
348
|
+
files: files
|
|
349
|
+
.filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
|
|
350
|
+
.map((f) => ({
|
|
351
|
+
path: typeof f.path === "string" ? f.path : "unknown",
|
|
352
|
+
linesPct: typeof f.linesPct === "number" ? f.linesPct : 0,
|
|
353
|
+
statementsPct: typeof f.statementsPct === "number" ? f.statementsPct : 0,
|
|
354
|
+
branchesPct: typeof f.branchesPct === "number" ? f.branchesPct : 0,
|
|
355
|
+
functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
|
|
356
|
+
})),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
break
|
|
360
|
+
}
|
|
361
|
+
case "argus_proxy_detection": {
|
|
362
|
+
if (record.isProxy === true) {
|
|
363
|
+
auditState.proxyContracts ??= []
|
|
364
|
+
auditState.proxyContracts.push({
|
|
365
|
+
file: typeof record.file === "string" ? record.file : "unknown",
|
|
366
|
+
proxyType: typeof record.proxyType === "string" ? record.proxyType : "unknown",
|
|
367
|
+
indicators: Array.isArray(record.indicators)
|
|
368
|
+
? record.indicators.filter((i): i is string => typeof i === "string")
|
|
369
|
+
: [],
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
break
|
|
373
|
+
}
|
|
374
|
+
case "argus_gas_analysis": {
|
|
375
|
+
const hotspots = record.hotspots
|
|
376
|
+
if (Array.isArray(hotspots)) {
|
|
377
|
+
auditState.gasHotspots = hotspots
|
|
378
|
+
.filter((h): h is Record<string, unknown> => !!h && typeof h === "object")
|
|
379
|
+
.map((h) => ({
|
|
380
|
+
contract: typeof h.contract === "string" ? h.contract : "unknown",
|
|
381
|
+
function: typeof h.function === "string" ? h.function : "unknown",
|
|
382
|
+
avgGas: typeof h.avgGas === "number" ? h.avgGas : 0,
|
|
383
|
+
}))
|
|
384
|
+
}
|
|
385
|
+
break
|
|
386
|
+
}
|
|
329
387
|
}
|
|
330
388
|
|
|
331
389
|
recordToolExecution(auditState, input.tool, findingsCount)
|
|
390
|
+
onStateChanged?.({ tool: input.tool, findingsCount })
|
|
332
391
|
}
|
|
333
392
|
}
|
package/src/hooks/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,54 +1,41 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
2
|
import { loadArgusConfig } from "./config/loader"
|
|
3
|
-
import { createHookGuard } from "./hooks/hook-system"
|
|
4
|
-
import { createTools } from "./create-tools"
|
|
5
3
|
import { createHooks } from "./create-hooks"
|
|
6
4
|
import { createManagers } from "./create-managers"
|
|
5
|
+
import { createTools } from "./create-tools"
|
|
6
|
+
import type { Dispatcher } from "./features/background-agent/background-manager"
|
|
7
|
+
import { createHookGuard } from "./hooks/hook-system"
|
|
7
8
|
import { createPluginInterface } from "./plugin-interface"
|
|
8
|
-
import {
|
|
9
|
-
import { createLogger } from "./shared/logger"
|
|
10
|
-
|
|
11
|
-
async function startSoloditMcp(port: number): Promise<void> {
|
|
12
|
-
const logger = createLogger()
|
|
13
|
-
|
|
14
|
-
// Health check before spawn: if already reachable, skip spawn
|
|
15
|
-
const health = await checkSoloditHealth(port, true)
|
|
16
|
-
if (health.reachable) {
|
|
17
|
-
logger.debug(`Solodit MCP already running on port ${port} — skipping spawn`)
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const child = Bun.spawn(["npx", "-y", "@lyuboslavlyubenov/solodit-mcp"], {
|
|
22
|
-
stdin: "ignore",
|
|
23
|
-
stdout: "ignore",
|
|
24
|
-
stderr: "ignore",
|
|
25
|
-
env: { ...process.env, PORT: String(port) },
|
|
26
|
-
})
|
|
27
|
-
child.unref()
|
|
28
|
-
|
|
29
|
-
// Health check after spawn: wait 2s, then ping
|
|
30
|
-
setTimeout(async () => {
|
|
31
|
-
const health = await checkSoloditHealth(port, true)
|
|
32
|
-
if (!health.reachable) {
|
|
33
|
-
logger.debug(`Solodit MCP not yet reachable on port ${port} — will retry on first use`)
|
|
34
|
-
} else {
|
|
35
|
-
logger.debug(`Solodit MCP healthy on port ${port}`)
|
|
36
|
-
}
|
|
37
|
-
}, 2000)
|
|
38
|
-
}
|
|
9
|
+
import { startSoloditMcp } from "./solodit-lifecycle"
|
|
39
10
|
|
|
40
11
|
const ArgusPlugin: Plugin = async (ctx) => {
|
|
41
12
|
const projectDir = ctx.directory ?? process.cwd()
|
|
42
13
|
const config = loadArgusConfig(projectDir)
|
|
43
14
|
|
|
44
15
|
if (config.solodit?.enabled !== false) {
|
|
45
|
-
|
|
46
|
-
// to avoid blocking plugin initialization
|
|
47
|
-
startSoloditMcp(config.solodit?.port ?? 3000)
|
|
16
|
+
await startSoloditMcp(config.solodit?.port ?? 3000)
|
|
48
17
|
}
|
|
49
18
|
|
|
50
19
|
const isHookEnabled = createHookGuard(config.disabled_hooks)
|
|
51
|
-
const
|
|
20
|
+
const taskCandidate = (ctx as Record<string, unknown>).task
|
|
21
|
+
const backgroundDispatcher: Dispatcher | undefined =
|
|
22
|
+
typeof taskCandidate === "function"
|
|
23
|
+
? async (agentName: string, prompt: string) => {
|
|
24
|
+
const result = await taskCandidate(agentName, prompt)
|
|
25
|
+
if (typeof result === "string") {
|
|
26
|
+
return result
|
|
27
|
+
}
|
|
28
|
+
if (typeof result === "object" && result !== null) {
|
|
29
|
+
const taskId = (result as Record<string, unknown>).task_id
|
|
30
|
+
if (typeof taskId === "string") {
|
|
31
|
+
return taskId
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return `task-${Date.now()}`
|
|
35
|
+
}
|
|
36
|
+
: undefined
|
|
37
|
+
|
|
38
|
+
const managers = createManagers({ projectDir, config, backgroundDispatcher })
|
|
52
39
|
const tools = createTools(config)
|
|
53
40
|
const hooks = createHooks({ config, managers, projectDir, isHookEnabled })
|
|
54
41
|
|