solidity-argus 0.3.7 → 0.5.6

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.
Files changed (107) hide show
  1. package/AGENTS.md +13 -6
  2. package/README.md +24 -12
  3. package/package.json +7 -3
  4. package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +1 -0
  5. package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +1 -0
  6. package/skills/checklists/cyfrin-defi-core/SKILL.md +1 -0
  7. package/skills/checklists/cyfrin-defi-integrations/SKILL.md +1 -0
  8. package/skills/checklists/cyfrin-gas/SKILL.md +1 -0
  9. package/skills/checklists/general-audit/SKILL.md +1 -0
  10. package/skills/methodology/audit-workflow/SKILL.md +1 -0
  11. package/skills/methodology/report-template/SKILL.md +1 -0
  12. package/skills/methodology/severity-classification/SKILL.md +1 -0
  13. package/skills/protocol-patterns/amm-dex/SKILL.md +1 -0
  14. package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +1 -0
  15. package/skills/protocol-patterns/dao-governance/SKILL.md +1 -0
  16. package/skills/protocol-patterns/lending-borrowing/SKILL.md +1 -0
  17. package/skills/protocol-patterns/staking-vesting/SKILL.md +1 -0
  18. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +0 -50
  19. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +0 -63
  20. package/src/agents/argus-prompt.ts +98 -33
  21. package/src/agents/pythia-prompt.ts +18 -1
  22. package/src/agents/scribe-prompt.ts +32 -10
  23. package/src/agents/sentinel-prompt.ts +19 -0
  24. package/src/agents/themis-prompt.ts +110 -0
  25. package/src/cli/commands/doctor.ts +29 -17
  26. package/src/config/loader.ts +29 -5
  27. package/src/config/schema.ts +45 -45
  28. package/src/constants/defaults.ts +1 -0
  29. package/src/create-hooks.ts +797 -148
  30. package/src/create-managers.ts +4 -2
  31. package/src/create-tools.ts +5 -1
  32. package/src/features/audit-enforcer/audit-enforcer.ts +1 -11
  33. package/src/features/background-agent/background-manager.ts +32 -5
  34. package/src/features/error-recovery/tool-error-recovery.ts +1 -0
  35. package/src/features/persistent-state/audit-state-manager.ts +272 -29
  36. package/src/features/persistent-state/event-sink.ts +96 -25
  37. package/src/features/persistent-state/findings-materializer.ts +34 -2
  38. package/src/features/persistent-state/global-run-index.ts +86 -8
  39. package/src/features/persistent-state/index.ts +7 -1
  40. package/src/features/persistent-state/run-finalizer.ts +116 -7
  41. package/src/features/persistent-state/run-pruner.ts +93 -0
  42. package/src/hooks/agent-tracker.ts +14 -2
  43. package/src/hooks/compaction-hook.ts +7 -16
  44. package/src/hooks/config-handler.ts +83 -29
  45. package/src/hooks/context-budget.ts +4 -5
  46. package/src/hooks/event-hook.ts +213 -57
  47. package/src/hooks/knowledge-sync-hook.ts +2 -3
  48. package/src/hooks/safe-create-hook.ts +13 -1
  49. package/src/hooks/system-prompt-hook.ts +20 -39
  50. package/src/hooks/tool-tracking-hook.ts +597 -323
  51. package/src/index.ts +15 -1
  52. package/src/knowledge/scvd-client.ts +2 -4
  53. package/src/knowledge/scvd-errors.ts +25 -2
  54. package/src/knowledge/scvd-index.ts +7 -5
  55. package/src/knowledge/scvd-sync.ts +6 -6
  56. package/src/managers/types.ts +20 -2
  57. package/src/shared/agent-names.ts +23 -0
  58. package/src/shared/audit-artifact-resolver.ts +8 -3
  59. package/src/shared/audit-phases.ts +12 -0
  60. package/src/shared/cache-paths.ts +41 -0
  61. package/src/shared/drop-diagnostics.ts +2 -2
  62. package/src/shared/forge-errors.ts +31 -0
  63. package/src/shared/forge-runner.ts +30 -0
  64. package/src/shared/format-error.ts +3 -0
  65. package/src/shared/index.ts +9 -0
  66. package/src/shared/key-tools.ts +39 -0
  67. package/src/shared/logger.ts +7 -7
  68. package/src/shared/path-containment.ts +25 -0
  69. package/src/shared/path-utils.ts +11 -0
  70. package/src/shared/report-path-resolver.ts +4 -2
  71. package/src/shared/safe-emit.ts +24 -0
  72. package/src/shared/token-utils.ts +5 -0
  73. package/src/shared/type-guards.ts +8 -0
  74. package/src/shared/validation-constants.ts +52 -0
  75. package/src/skills/analysis/cluster.ts +1 -114
  76. package/src/skills/analysis/normalize.ts +2 -114
  77. package/src/skills/analysis/stopwords.ts +109 -0
  78. package/src/skills/argus-skill-resolver.ts +6 -3
  79. package/src/solodit-lifecycle.ts +153 -37
  80. package/src/state/adapters.ts +60 -66
  81. package/src/state/finding-aggregation.ts +6 -8
  82. package/src/state/finding-fingerprint.ts +1 -1
  83. package/src/state/finding-store.ts +31 -9
  84. package/src/state/index.ts +1 -1
  85. package/src/state/projectors.ts +27 -19
  86. package/src/state/schemas.ts +8 -32
  87. package/src/state/types.ts +3 -0
  88. package/src/tools/contract-analyzer-tool.ts +4 -6
  89. package/src/tools/forge-coverage-tool.ts +10 -35
  90. package/src/tools/forge-fuzz-tool.ts +21 -51
  91. package/src/tools/forge-test-tool.ts +25 -47
  92. package/src/tools/gas-analysis-tool.ts +12 -41
  93. package/src/tools/pattern-checker-tool.ts +37 -15
  94. package/src/tools/pattern-loader.ts +18 -4
  95. package/src/tools/persist-deduped-tool.ts +94 -0
  96. package/src/tools/proxy-detection-tool.ts +35 -34
  97. package/src/tools/read-findings-tool.ts +390 -0
  98. package/src/tools/record-finding-tool.ts +120 -25
  99. package/src/tools/report-generator-tool.ts +394 -328
  100. package/src/tools/report-preflight.ts +5 -1
  101. package/src/tools/slither-tool.ts +55 -16
  102. package/src/tools/solodit-search-tool.ts +260 -112
  103. package/src/tools/sync-knowledge-tool.ts +2 -3
  104. package/src/utils/solidity-parser.ts +39 -24
  105. package/src/features/migration/index.ts +0 -14
  106. package/src/features/migration/migration-adapter.ts +0 -151
  107. package/src/features/migration/parity-telemetry.ts +0 -133
@@ -1,17 +1,17 @@
1
1
  import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
2
+ import { ARGUS_FAMILY } from "../shared/agent-names"
2
3
 
3
4
  type ChatParamsInput = Parameters<NonNullable<PluginHooks["chat.params"]>>[0] & {
4
5
  agent?: string
5
6
  }
6
7
  type ChatMessageInput = Parameters<NonNullable<PluginHooks["chat.message"]>>[0]
7
8
 
8
- const ARGUS_FAMILY = new Set(["argus", "sentinel", "pythia", "scribe"])
9
-
10
9
  export type AgentTracker = ReturnType<typeof createAgentTracker>
11
10
 
12
11
  export function createAgentTracker() {
13
12
  const sessions = new Map<string, string>()
14
13
  const childSessions = new Map<string, Set<string>>()
14
+ const parentByChild = new Map<string, string>()
15
15
 
16
16
  const trackSession = (sessionID: string, agent?: string): void => {
17
17
  if (!agent) {
@@ -45,6 +45,13 @@ export function createAgentTracker() {
45
45
 
46
46
  clearSession: (sessionID: string): void => {
47
47
  sessions.delete(sessionID)
48
+ const children = childSessions.get(sessionID)
49
+ if (children) {
50
+ for (const child of children) {
51
+ parentByChild.delete(child)
52
+ }
53
+ }
54
+ childSessions.delete(sessionID)
48
55
  },
49
56
 
50
57
  getTrackedSessions: (): Map<string, string> => {
@@ -58,11 +65,16 @@ export function createAgentTracker() {
58
65
  childSessions.set(parentSessionId, children)
59
66
  }
60
67
  children.add(childSessionId)
68
+ parentByChild.set(childSessionId, parentSessionId)
61
69
  },
62
70
 
63
71
  getChildSessions: (parentSessionId: string): string[] => {
64
72
  const children = childSessions.get(parentSessionId)
65
73
  return children ? Array.from(children) : []
66
74
  },
75
+
76
+ getParentSession: (childSessionId: string): string | undefined => {
77
+ return parentByChild.get(childSessionId)
78
+ },
67
79
  }
68
80
  }
@@ -1,28 +1,19 @@
1
- import type { AuditState, FindingSeverity } from "../state/types"
1
+ import { countBySeverity } from "../shared/validation-constants"
2
+ import type { AuditState } from "../state/types"
2
3
  import type { ReconContext } from "./recon-context-builder"
3
4
  import { buildReconContextBlock } from "./recon-context-builder"
4
5
 
5
6
  export function createCompactionHook(
6
- getAuditState: () => AuditState | null,
7
+ getAuditState: (sessionId?: string) => AuditState | null,
7
8
  getReconContext?: () => ReconContext | null,
8
- ): (input: { summary: string }) => Promise<string | null> {
9
- return async (_input: { summary: string }): Promise<string | null> => {
10
- const state = getAuditState()
9
+ ): (input: { summary: string; sessionId?: string }) => Promise<string | null> {
10
+ return async (input: { summary: string; sessionId?: string }): Promise<string | null> => {
11
+ const state = getAuditState(input.sessionId)
11
12
 
12
13
  const parts: string[] = []
13
14
 
14
15
  if (state) {
15
- const severityCounts: Record<FindingSeverity, number> = {
16
- Critical: 0,
17
- High: 0,
18
- Medium: 0,
19
- Low: 0,
20
- Informational: 0,
21
- }
22
-
23
- for (const finding of state.findings) {
24
- severityCounts[finding.severity]++
25
- }
16
+ const severityCounts = countBySeverity(state.findings)
26
17
 
27
18
  const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
28
19
  const contracts = state.contractsReviewed.join(", ")
@@ -1,20 +1,22 @@
1
1
  import { existsSync, readdirSync } from "node:fs"
2
- import { homedir } from "node:os"
3
2
  import { join, resolve } from "node:path"
4
3
  import type { Config } from "@opencode-ai/sdk/v2"
5
4
  import { ARGUS_PROMPT } from "../agents/argus-prompt"
6
5
  import { PYTHIA_PROMPT } from "../agents/pythia-prompt"
7
6
  import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
8
7
  import { SENTINEL_PROMPT } from "../agents/sentinel-prompt"
8
+ import { THEMIS_PROMPT } from "../agents/themis-prompt"
9
9
  import type { ArgusConfig } from "../config/types"
10
10
  import { DEFAULT_MODELS, DEFAULT_STEPS } from "../constants/defaults"
11
+ import { getTrailOfBitsCacheDir } from "../shared/cache-paths"
11
12
  import { createLogger } from "../shared/logger"
12
13
  import { createKnowledgeSyncHook } from "./knowledge-sync-hook"
13
14
 
14
- const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
15
15
  const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
16
16
  const TOB_BRANCH = "main"
17
17
  let tobCloneInFlight = false
18
+ let tobCloneStartedAt: number | null = null
19
+ const TOB_CLONE_STUCK_TIMEOUT_MS = 120_000
18
20
 
19
21
  function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
20
22
  const pluginsDir = join(rootDir, "plugins")
@@ -35,30 +37,67 @@ function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
35
37
  }
36
38
 
37
39
  function ensureTrailOfBitsSkills(): string[] {
38
- if (existsSync(TOB_CACHE_DIR)) {
39
- return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
40
+ const cacheDir = getTrailOfBitsCacheDir()
41
+
42
+ if (existsSync(cacheDir)) {
43
+ return getTrailOfBitsSkillsPaths(cacheDir)
44
+ }
45
+
46
+ // Reset stuck flag if clone has been in-flight for more than 120 seconds
47
+ if (
48
+ tobCloneInFlight &&
49
+ tobCloneStartedAt !== null &&
50
+ Date.now() - tobCloneStartedAt > TOB_CLONE_STUCK_TIMEOUT_MS
51
+ ) {
52
+ const logger = createLogger()
53
+ logger.warn(
54
+ `Trail of Bits skills clone flag stuck for >${TOB_CLONE_STUCK_TIMEOUT_MS / 1000}s — resetting (URL: ${TOB_REPO_URL}, dir: ${cacheDir})`,
55
+ )
56
+ tobCloneInFlight = false
57
+ tobCloneStartedAt = null
40
58
  }
41
59
 
42
60
  if (!tobCloneInFlight) {
43
61
  tobCloneInFlight = true
44
- const cloneProcess = Bun.spawn(
45
- ["git", "clone", "--depth", "1", "--branch", TOB_BRANCH, TOB_REPO_URL, TOB_CACHE_DIR],
46
- {
47
- stdin: "ignore",
48
- stdout: "ignore",
49
- stderr: "ignore",
50
- signal: AbortSignal.timeout(60_000),
51
- },
52
- )
62
+ tobCloneStartedAt = Date.now()
63
+ let cloneProcess: ReturnType<typeof Bun.spawn>
64
+ try {
65
+ cloneProcess = Bun.spawn(
66
+ ["git", "clone", "--depth", "1", "--branch", TOB_BRANCH, TOB_REPO_URL, cacheDir],
67
+ {
68
+ stdin: "ignore",
69
+ stdout: "ignore",
70
+ stderr: "ignore",
71
+ signal: AbortSignal.timeout(60_000),
72
+ },
73
+ )
74
+ } catch (spawnErr) {
75
+ const logger = createLogger()
76
+ logger.error(
77
+ `Trail of Bits skills clone spawn failed (URL: ${TOB_REPO_URL}, dir: ${cacheDir}): ${spawnErr instanceof Error ? spawnErr.message : String(spawnErr)}`,
78
+ )
79
+ tobCloneInFlight = false
80
+ tobCloneStartedAt = null
81
+ return []
82
+ }
53
83
  cloneProcess.exited
54
84
  .then((code) => {
55
85
  if (code !== 0) {
56
86
  const logger = createLogger()
57
- logger.warn(`Trail of Bits skills clone failed with exit code ${code}`)
87
+ logger.warn(
88
+ `Trail of Bits skills clone failed with exit code ${code} (URL: ${TOB_REPO_URL}, dir: ${cacheDir})`,
89
+ )
58
90
  }
59
91
  })
92
+ .catch((err) => {
93
+ const logger = createLogger()
94
+ logger.error(
95
+ `Trail of Bits skills clone process error (URL: ${TOB_REPO_URL}, dir: ${cacheDir}): ${err instanceof Error ? err.message : String(err)}`,
96
+ )
97
+ })
60
98
  .finally(() => {
61
99
  tobCloneInFlight = false
100
+ tobCloneStartedAt = null
62
101
  })
63
102
  }
64
103
 
@@ -72,8 +111,8 @@ export function createConfigHandler(
72
111
  const triggerKnowledgeSync = createKnowledgeSyncHook(argusConfig)
73
112
 
74
113
  return async (config: Config): Promise<void> => {
75
- config.agent = {
76
- ...config.agent,
114
+ config.agent ??= {}
115
+ Object.assign(config.agent, {
77
116
  argus: {
78
117
  mode: "primary",
79
118
  model: argusConfig.agents?.argus?.model ?? DEFAULT_MODELS.argus,
@@ -89,6 +128,7 @@ export function createConfigHandler(
89
128
  sentinel: "allow",
90
129
  pythia: "allow",
91
130
  scribe: "allow",
131
+ themis: "allow",
92
132
  },
93
133
  skill: "allow",
94
134
  },
@@ -108,6 +148,7 @@ export function createConfigHandler(
108
148
  argus_check_patterns: "allow",
109
149
  argus_proxy_detection: "allow",
110
150
  argus_forge_coverage: "allow",
151
+ argus_record_finding: "allow",
111
152
  argus_skill_load: "allow",
112
153
  skill: "allow",
113
154
  },
@@ -121,6 +162,7 @@ export function createConfigHandler(
121
162
  permission: {
122
163
  argus_solodit_search: "allow",
123
164
  argus_check_patterns: "allow",
165
+ argus_record_finding: "allow",
124
166
  argus_skill_load: "allow",
125
167
  skill: "allow",
126
168
  },
@@ -132,22 +174,36 @@ export function createConfigHandler(
132
174
  description: "Audit report writer",
133
175
  prompt: SCRIBE_PROMPT,
134
176
  permission: {
177
+ argus_read_findings: "allow",
135
178
  argus_generate_report: "allow",
179
+ argus_persist_deduped: "allow",
136
180
  argus_skill_load: "allow",
137
181
  skill: "allow",
138
182
  },
139
183
  },
140
- }
184
+ themis: {
185
+ mode: "subagent",
186
+ model: argusConfig.agents?.themis?.model ?? DEFAULT_MODELS.themis,
187
+ steps: argusConfig.agents?.themis?.steps ?? DEFAULT_STEPS,
188
+ description: "Audit quality gate — independent cross-validation (GPT-5.4)",
189
+ prompt: THEMIS_PROMPT,
190
+ permission: {
191
+ argus_read_findings: "allow",
192
+ argus_solodit_search: "allow",
193
+ argus_check_patterns: "allow",
194
+ argus_skill_load: "allow",
195
+ skill: "allow",
196
+ },
197
+ },
198
+ })
141
199
 
142
200
  if (argusConfig.solodit?.enabled !== false) {
143
- const port = argusConfig.solodit?.port ?? 3000
144
- config.mcp = {
145
- ...(config.mcp ?? {}),
146
- "solodit-mcp": {
147
- type: "remote",
148
- url: `http://localhost:${port}/mcp`,
149
- enabled: true,
150
- },
201
+ const port = argusConfig.solodit?.port ?? 54173
202
+ config.mcp ??= {}
203
+ config.mcp["solodit-mcp"] = {
204
+ type: "remote",
205
+ url: `http://localhost:${port}/mcp`,
206
+ enabled: true,
151
207
  }
152
208
  }
153
209
 
@@ -167,10 +223,8 @@ export function createConfigHandler(
167
223
  const tobSkillDirs = ensureTrailOfBitsSkills()
168
224
  if (tobSkillDirs.length > 0) skillsPaths.push(...tobSkillDirs)
169
225
 
170
- config.skills = {
171
- ...(config.skills ?? {}),
172
- paths: skillsPaths,
173
- }
226
+ config.skills ??= {}
227
+ config.skills.paths = skillsPaths
174
228
 
175
229
  if (argusConfig.knowledge?.autoSync !== false) {
176
230
  triggerKnowledgeSync()
@@ -8,14 +8,13 @@
8
8
  * Decoupled from system-prompt-hook.ts — consumed by the hook when available.
9
9
  */
10
10
 
11
+ import { ARGUS_ORCHESTRATOR, ARGUS_SUBAGENTS } from "../shared/agent-names"
12
+
11
13
  const ARGUS_BUDGET = 2000
12
14
  const SUBAGENT_BUDGET = 1000
13
15
  const PRESSURE_THRESHOLD = 0.7
14
16
  const PRESSURE_REDUCTION = 0.5
15
17
 
16
- const ARGUS_AGENTS = new Set(["argus"])
17
- const SUBAGENTS = new Set(["sentinel", "pythia", "scribe"])
18
-
19
18
  /**
20
19
  * Returns the token budget for a given agent, adjusted for context pressure.
21
20
  *
@@ -26,9 +25,9 @@ const SUBAGENTS = new Set(["sentinel", "pythia", "scribe"])
26
25
  export function getTokenBudgetForAgent(agent: string, contextPressure: number = 0): number {
27
26
  let budget: number
28
27
 
29
- if (ARGUS_AGENTS.has(agent)) {
28
+ if (ARGUS_ORCHESTRATOR.has(agent)) {
30
29
  budget = ARGUS_BUDGET
31
- } else if (SUBAGENTS.has(agent)) {
30
+ } else if (ARGUS_SUBAGENTS.has(agent)) {
32
31
  budget = SUBAGENT_BUDGET
33
32
  } else {
34
33
  return 0