solidity-argus 0.1.7 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidity-argus",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Solidity smart contract security auditing plugin for OpenCode — 4 specialized agents, 8 tools, and a curated vulnerability knowledge base",
5
5
  "keywords": ["solidity", "security", "audit", "opencode", "plugin", "smart-contract", "ethereum", "defi", "slither", "foundry"],
6
6
  "author": "Apegurus",
@@ -272,14 +272,14 @@ Your subagents have access to these specialized tools. Know when to delegate eac
272
272
  You have access to OpenCode Skills through the \`skill\` tool. Skills are specialized knowledge modules and must be used proactively when they improve audit accuracy.
273
273
 
274
274
  - **Curated skill map (load these first)**:
275
- - **Reconnaissance**: \`protocol-patterns/amm-dex\`, \`protocol-patterns/lending-borrowing\`, \`protocol-patterns/bridges-cross-chain\`
276
- - **Manual Review**: \`vulnerability-patterns/reentrancy\`, \`vulnerability-patterns/oracle-manipulation\`, \`vulnerability-patterns/access-control\`
277
- - **Verification**: \`checklists/cyfrin-defi-core\`, \`methodology/severity-classification\`, \`methodology/report-template\`
275
+ - **Reconnaissance**: \`amm-dex\`, \`lending-borrowing\`, \`bridges-cross-chain\`
276
+ - **Manual Review**: \`reentrancy\`, \`oracle-manipulation\`, \`access-control\`
277
+ - **Verification**: \`cyfrin-defi-core\`, \`severity-classification\`, \`report-template\`
278
278
 
279
279
  - **Deterministic trigger rules**:
280
- - If the protocol uses AMM reserves or pool math, load \`protocol-patterns/amm-dex\` before Attack Surface Mapping.
281
- - If price feeds or spot prices influence critical state changes, load \`vulnerability-patterns/oracle-manipulation\` before severity assessment.
282
- - If proxy/upgrade patterns are present, load \`checklists/cyfrin-best-practices-upgrades\` before final recommendations.
280
+ - If the protocol uses AMM reserves or pool math, load \`amm-dex\` before Attack Surface Mapping.
281
+ - If price feeds or spot prices influence critical state changes, load \`oracle-manipulation\` before severity assessment.
282
+ - If proxy/upgrade patterns are present, load \`cyfrin-best-practices-upgrades\` before final recommendations.
283
283
 
284
284
  - **Trail of Bits skills**:
285
285
  - For pre-audit deep context modeling and attack-surface grounding: \`audit-context-building\`
@@ -91,20 +91,20 @@ OpenCode has a powerful **Skills** system that allows you to load specialized kn
91
91
 
92
92
  **How to use**:
93
93
  - Load a relevant skill before deep research when protocol context is non-trivial.
94
- - Prioritize \`vulnerability-patterns/*\`, \`protocol-patterns/*\`, and \`references/*\` skills for exploit precedent mapping.
94
+ - Prioritize vulnerability pattern skills, protocol pattern skills, and reference skills for exploit precedent mapping.
95
95
  - Use the \`skill\` tool directly when available to load the exact skill you need.
96
96
  - **Curated skill map**:
97
- - \`vulnerability-patterns/reentrancy\`, \`vulnerability-patterns/oracle-manipulation\`, \`vulnerability-patterns/flash-loan-attacks\`
98
- - \`protocol-patterns/lending-borrowing\`, \`protocol-patterns/amm-dex\`
99
- - \`references/exploit-reference\`
97
+ - \`reentrancy\`, \`oracle-manipulation\`, \`flash-loan-attacks\`
98
+ - \`lending-borrowing\`, \`amm-dex\`
99
+ - \`exploit-reference\`
100
100
  - **Deterministic trigger rules**:
101
- - If you investigate spot-price dependencies, load \`vulnerability-patterns/oracle-manipulation\` first.
102
- - If capital-efficient attacks or same-block loops are plausible, load \`vulnerability-patterns/flash-loan-attacks\` first.
103
- - If the protocol integrates arbitrary ERC20s, load ToB \`token-integration-analyzer\` (building-secure-contracts plugin) before recommendation drafting.
101
+ - If you investigate spot-price dependencies, load \`oracle-manipulation\` first.
102
+ - If capital-efficient attacks or same-block loops are plausible, load \`flash-loan-attacks\` first.
103
+ - If the protocol integrates arbitrary ERC20s, load ToB \`token-integration-analyzer\` (building-secure-contracts plugin) before recommendation drafting.
104
104
  - **Examples**:
105
- - "I am loading \`reentrancy\` to cross-reference known exploit patterns and missed edge cases."
106
- - "I am loading \`lending-borrowing\` to map lending-specific oracle and liquidation failure modes."
107
- - "I am loading \`audit-context-building\` (Trail of Bits) to build a line-by-line system model before vulnerability hypothesis generation."
105
+ - "I am loading \`reentrancy\` to cross-reference known exploit patterns and missed edge cases."
106
+ - "I am loading \`lending-borrowing\` to map lending-specific oracle and liquidation failure modes."
107
+ - "I am loading \`audit-context-building\` (Trail of Bits) to build a line-by-line system model before vulnerability hypothesis generation."
108
108
  - You are a generalist researcher. Use Skills to become a specialist on demand.
109
109
 
110
110
  ## OUTPUT FORMAT
@@ -65,12 +65,12 @@ Before generating the report, verify:
65
65
  Use the \`skill\` tool when needed to improve report quality and consistency.
66
66
 
67
67
  - **Curated skill map**:
68
- - \`methodology/report-template\`, \`methodology/severity-classification\`
69
- - \`checklists/cyfrin-defi-core\`
70
- - \`references/exploit-reference\`
68
+ - \`report-template\`, \`severity-classification\`
69
+ - \`cyfrin-defi-core\`
70
+ - \`exploit-reference\`
71
71
  - **Deterministic trigger rules**:
72
- - If severity wording drifts, load \`methodology/severity-classification\` before publishing.
73
- - If recommendation quality is generic, load \`checklists/cyfrin-defi-core\` before final edits.
72
+ - If severity wording drifts, load \`severity-classification\` before publishing.
73
+ - If recommendation quality is generic, load \`cyfrin-defi-core\` before final edits.
74
74
 
75
75
  ## OUTPUT FORMAT
76
76
 
@@ -92,13 +92,13 @@ You have access to a specific set of tools. Use them effectively.
92
92
  Use the \`skill\` tool to load specialized skills before deep verification work.
93
93
 
94
94
  - **Curated skill map**:
95
- - \`vulnerability-patterns/reentrancy\`, \`vulnerability-patterns/access-control\`, \`vulnerability-patterns/oracle-manipulation\`
96
- - \`checklists/cyfrin-defi-integrations\`, \`methodology/severity-classification\`
97
- - Trail of Bits: \`property-based-testing\`, \`variant-analysis\`
95
+ - \`reentrancy\`, \`access-control\`, \`oracle-manipulation\`
96
+ - \`cyfrin-defi-integrations\`, \`severity-classification\`
97
+ - Trail of Bits: \`property-based-testing\`, \`variant-analysis\`
98
98
  - **Deterministic trigger rules**:
99
- - If external calls and mutable state interleave, load \`vulnerability-patterns/reentrancy\` before writing PoCs.
100
- - If privileged flows are central to the finding, load \`vulnerability-patterns/access-control\` before severity scoring.
101
- - If fuzzing strategy is unclear, load ToB \`property-based-testing\` before selecting invariants.
99
+ - If external calls and mutable state interleave, load \`reentrancy\` before writing PoCs.
100
+ - If privileged flows are central to the finding, load \`access-control\` before severity scoring.
101
+ - If fuzzing strategy is unclear, load ToB \`property-based-testing\` before selecting invariants.
102
102
 
103
103
  ## OUTPUT FORMAT
104
104
 
@@ -2,13 +2,14 @@ import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
2
2
  import type { ArgusConfig } from "./config/types"
3
3
  import type { Managers } from "./managers/types"
4
4
  import type { HookName } from "./hooks/types"
5
- import { createAuditState } from "./state/audit-state"
6
5
  import { createConfigHandler } from "./hooks/config-handler"
7
- import { createSystemPromptHook } from "./hooks/system-prompt-hook"
8
6
  import { createCompactionHook } from "./hooks/compaction-hook"
9
7
  import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
10
8
  import { createEventHookV2 } from "./hooks/event-hook-v2"
11
9
  import { safeCreateHook } from "./hooks/safe-create-hook"
10
+ import { createToolOutputTruncator } from "./features/context-monitor"
11
+ import { createSessionRecoveryHandler } from "./features/error-recovery"
12
+ import { createToolErrorRecoveryHandler } from "./features/error-recovery"
12
13
 
13
14
  export type Hooks = Pick<
14
15
  PluginHooks,
@@ -25,49 +26,62 @@ export function createHooks(args: {
25
26
  projectDir: string
26
27
  isHookEnabled: (name: HookName) => boolean
27
28
  }): Hooks {
28
- const { config, projectDir, isHookEnabled } = args
29
+ const { config, managers, projectDir, isHookEnabled } = args
30
+ const { auditStateManager, backgroundManager } = managers
29
31
 
30
- const { state: auditState, store: findingStore } = createAuditState(projectDir)
31
- const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir)
32
- setAuditState(auditState)
32
+ const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
33
+ const toolErrorRecoveryHandler = createToolErrorRecoveryHandler()
34
+ const outputTruncator = createToolOutputTruncator()
33
35
 
34
- const systemPromptHook = isHookEnabled("system-prompt")
35
- ? safeCreateHook(
36
- () =>
37
- createSystemPromptHook(getAuditState, {
38
- argusConfig: config,
39
- projectDir,
40
- }),
41
- "system-prompt"
42
- )
43
- : undefined
36
+ const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
37
+ async ({ type, auditState, setAuditState: setState }) => {
38
+ if (type === "session.created") {
39
+ const recoveredState = await auditStateManager.load()
40
+ if (recoveredState) {
41
+ setState(recoveredState)
42
+ }
43
+ return
44
+ }
45
+
46
+ if (type === "session.idle" && auditState) {
47
+ await auditStateManager.save(auditState)
48
+ return
49
+ }
50
+
51
+ if (type === "session.deleted") {
52
+ await auditStateManager.reset()
53
+ }
54
+ },
55
+ async ({ type, sessionId, setAuditState: setState }) => {
56
+ await sessionRecoveryHandler({ type, sessionId, setAuditState: setState })
57
+ },
58
+ async ({ type }) => {
59
+ if (type === "session.idle") {
60
+ backgroundManager.getActiveCount()
61
+ }
62
+ },
63
+ ])
44
64
 
45
- const compactionHook = isHookEnabled("compaction")
65
+ const initialState = auditStateManager.get()
66
+ if (initialState) {
67
+ setAuditState(initialState)
68
+ }
69
+
70
+ const compactionHook = isHookEnabled("compaction")
46
71
  ? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
47
72
  : undefined
48
73
 
49
74
  const toolTrackingHook = isHookEnabled("tool-tracking")
50
- ? safeCreateHook(
51
- () => createToolTrackingHook(auditState, findingStore),
52
- "tool-tracking"
53
- )
75
+ ? safeCreateHook(() => createToolTrackingHook(getAuditState), "tool-tracking")
54
76
  : undefined
55
77
 
56
78
  const safeEventHook = isHookEnabled("event")
57
79
  ? safeCreateHook(() => eventHook, "event")
58
80
  : undefined
59
81
 
60
- return {
61
- config: createConfigHandler(config, projectDir),
62
- "experimental.chat.system.transform": systemPromptHook
63
- ? async (_input, output) => {
64
- const block = await systemPromptHook({
65
- system: output.system.join("\n\n"),
66
- cwd: projectDir,
67
- })
68
- if (block) output.system.push(block)
69
- }
70
- : undefined,
82
+ return {
83
+ config: createConfigHandler(config, projectDir),
84
+ "experimental.chat.system.transform": undefined,
71
85
  "experimental.session.compacting": compactionHook
72
86
  ? async (_input, output) => {
73
87
  const block = await compactionHook({ summary: output.context.join("\n") })
@@ -76,11 +90,21 @@ export function createHooks(args: {
76
90
  : undefined,
77
91
  "tool.execute.after": toolTrackingHook
78
92
  ? async (input, output) => {
93
+ const recoveryHint = toolErrorRecoveryHandler({
94
+ tool: input.tool,
95
+ result: output.output,
96
+ })
97
+
79
98
  await toolTrackingHook({
80
99
  tool: input.tool,
81
100
  args: input.args,
82
101
  result: output.output,
83
102
  })
103
+
104
+ const outputWithHint = recoveryHint
105
+ ? `${output.output}${recoveryHint}`
106
+ : output.output
107
+ output.output = outputTruncator(outputWithHint)
84
108
  }
85
109
  : undefined,
86
110
  event: safeEventHook,
@@ -1,3 +1,4 @@
1
+ import type { AuditState } from "../../state/types"
1
2
  import type { AuditStateManager } from "../../managers/types"
2
3
  import { createLogger } from "../../shared/logger"
3
4
 
@@ -6,7 +7,11 @@ export function createSessionRecoveryHandler(
6
7
  ) {
7
8
  const logger = createLogger()
8
9
 
9
- return async (event: { type: string; sessionId?: string }): Promise<void> => {
10
+ return async (event: {
11
+ type: string
12
+ sessionId?: string
13
+ setAuditState?: (state: AuditState | null) => void
14
+ }): Promise<void> => {
10
15
  if (event.type !== "session.error") return
11
16
 
12
17
  logger.info("Session error detected, attempting state recovery...")
@@ -14,6 +19,7 @@ export function createSessionRecoveryHandler(
14
19
  try {
15
20
  const recovered = await auditStateManager.load()
16
21
  if (recovered) {
22
+ event.setAuditState?.(recovered)
17
23
  logger.info(
18
24
  `State recovered: phase=${recovered.currentPhase}, findings=${recovered.findings.length}`,
19
25
  )
@@ -1,7 +1,6 @@
1
1
  import { resolve, join } from "node:path"
2
2
  import { existsSync, readdirSync } from "node:fs"
3
3
  import { homedir } from "node:os"
4
- import { execSync } from "node:child_process"
5
4
  import type { Config } from "@opencode-ai/sdk/v2"
6
5
  import type { ArgusConfig } from "../config/types"
7
6
  import { DEFAULT_MODELS } from "../constants/defaults"
@@ -13,6 +12,7 @@ import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
13
12
 
14
13
  const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
15
14
  const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
15
+ let tobCloneInFlight = false
16
16
 
17
17
  function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
18
18
  const pluginsDir = join(rootDir, "plugins")
@@ -33,16 +33,26 @@ function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
33
33
  }
34
34
 
35
35
  function ensureTrailOfBitsSkills(): string[] {
36
- if (existsSync(TOB_CACHE_DIR)) return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
37
- try {
38
- execSync(`git clone --depth 1 ${TOB_REPO_URL} "${TOB_CACHE_DIR}"`, {
39
- stdio: "ignore",
40
- timeout: 30_000,
41
- })
36
+ if (existsSync(TOB_CACHE_DIR)) {
42
37
  return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
43
- } catch (_e) {
44
- return []
45
38
  }
39
+
40
+ if (!tobCloneInFlight) {
41
+ tobCloneInFlight = true
42
+ const cloneProcess = Bun.spawn(
43
+ ["git", "clone", "--depth", "1", TOB_REPO_URL, TOB_CACHE_DIR],
44
+ {
45
+ stdin: "ignore",
46
+ stdout: "ignore",
47
+ stderr: "ignore",
48
+ },
49
+ )
50
+ cloneProcess.exited.finally(() => {
51
+ tobCloneInFlight = false
52
+ })
53
+ }
54
+
55
+ return []
46
56
  }
47
57
 
48
58
  export function createConfigHandler(
@@ -1,4 +1,4 @@
1
- import type { AuditState, AuditPhase } from "../state/types"
1
+ import type { AuditState } from "../state/types"
2
2
  import { createAuditState } from "../state/audit-state"
3
3
  import { createLogger } from "../shared/logger"
4
4
 
@@ -19,6 +19,7 @@ export type EventSubHandler = (event: {
19
19
  type: string
20
20
  sessionId?: string
21
21
  auditState: AuditState | null
22
+ setAuditState: (state: AuditState | null) => void
22
23
  }) => Promise<void>
23
24
 
24
25
  export function createEventHookV2(
@@ -82,7 +83,12 @@ export function createEventHookV2(
82
83
 
83
84
  for (const handler of subHandlers) {
84
85
  try {
85
- await handler({ type, sessionId, auditState: currentAuditState })
86
+ await handler({
87
+ type,
88
+ sessionId,
89
+ auditState: currentAuditState,
90
+ setAuditState,
91
+ })
86
92
  } catch (error) {
87
93
  logger.error(`Sub-handler failed for event ${type}:`, error)
88
94
  }
@@ -1,5 +1,6 @@
1
1
  import type { AuditState, FindingSeverity } from "../state/types"
2
2
  import type { FindingStore } from "../state/finding-store"
3
+ import { createFindingStore } from "../state/finding-store"
3
4
 
4
5
  type ToolHookInput = {
5
6
  tool: string
@@ -193,14 +194,33 @@ function recordToolExecution(
193
194
  * Findings are deduplicated via the FindingStore (by check+file+lines).
194
195
  */
195
196
  export function createToolTrackingHook(
196
- auditState: AuditState,
197
- store: FindingStore
197
+ getAuditState: () => AuditState | null
198
198
  ): (input: ToolHookInput) => Promise<void> {
199
+ const storesByState = new WeakMap<AuditState, FindingStore>()
200
+
201
+ function resolveStateAndStore(): { state: AuditState; store: FindingStore } | null {
202
+ const state = getAuditState()
203
+ if (!state) return null
204
+
205
+ let store = storesByState.get(state)
206
+ if (!store) {
207
+ store = createFindingStore(state)
208
+ storesByState.set(state, store)
209
+ }
210
+
211
+ return { state, store }
212
+ }
213
+
199
214
  return async (input: ToolHookInput): Promise<void> => {
200
215
  if (!input.tool.startsWith("argus_")) {
201
216
  return
202
217
  }
203
218
 
219
+ const resolved = resolveStateAndStore()
220
+ if (!resolved) return
221
+
222
+ const { state: auditState, store } = resolved
223
+
204
224
  let parsed: unknown
205
225
  try {
206
226
  parsed = JSON.parse(input.result)
@@ -4,7 +4,6 @@
4
4
  */
5
5
 
6
6
  export type HookName =
7
- | "system-prompt"
8
7
  | "compaction"
9
8
  | "tool-tracking"
10
9
  | "event"
package/src/index.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin"
2
- import { spawn } from "node:child_process"
3
2
  import { loadArgusConfig } from "./config/loader"
4
3
  import { createHookGuard } from "./hooks/hook-system"
5
4
  import { createTools } from "./create-tools"
@@ -8,13 +7,13 @@ import { createManagers } from "./create-managers"
8
7
  import { createPluginInterface } from "./plugin-interface"
9
8
 
10
9
  function startSoloditMcp(port: number): void {
11
- const child = spawn("npx", ["-y", "@lyuboslavlyubenov/solodit-mcp"], {
12
- stdio: "ignore",
13
- detached: false,
10
+ const child = Bun.spawn(["npx", "-y", "@lyuboslavlyubenov/solodit-mcp"], {
11
+ stdin: "ignore",
12
+ stdout: "ignore",
13
+ stderr: "ignore",
14
14
  env: { ...process.env, PORT: String(port) },
15
15
  })
16
16
  child.unref()
17
- child.on("error", () => {})
18
17
  }
19
18
 
20
19
  const ArgusPlugin: Plugin = async (ctx) => {
@@ -11,17 +11,12 @@ export function createPluginInterface(args: {
11
11
  }): PluginReturn {
12
12
  const { tools, hooks } = args
13
13
 
14
- const result: PluginReturn = {
15
- tool: tools,
16
- config: hooks.config,
17
- }
18
-
19
- if (hooks["experimental.chat.system.transform"]) {
20
- result["experimental.chat.system.transform"] =
21
- hooks["experimental.chat.system.transform"]
22
- }
14
+ const result: PluginReturn = {
15
+ tool: tools,
16
+ config: hooks.config,
17
+ }
23
18
 
24
- if (hooks["experimental.session.compacting"]) {
19
+ if (hooks["experimental.session.compacting"]) {
25
20
  result["experimental.session.compacting"] =
26
21
  hooks["experimental.session.compacting"]
27
22
  }
@@ -1,257 +0,0 @@
1
- import { existsSync, readdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join, resolve } from "node:path";
4
- import type { ArgusConfig } from "../config/types";
5
- import type { AuditState, FindingSeverity } from "../state/types";
6
-
7
- interface SystemPromptInput {
8
- system: string;
9
- cwd: string;
10
- }
11
-
12
- interface SkillIndexEntry {
13
- count: number;
14
- sample: string[];
15
- }
16
-
17
- interface SkillIndexSnapshot {
18
- bundled: SkillIndexEntry;
19
- trailOfBits: SkillIndexEntry;
20
- custom: SkillIndexEntry;
21
- }
22
-
23
- const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills");
24
-
25
- /**
26
- * Checks if the given directory contains a Solidity project
27
- * by looking for foundry.toml or hardhat.config.{js,ts}
28
- */
29
- async function isSolidityProject(cwd: string): Promise<boolean> {
30
- const checks = [
31
- Bun.file(join(cwd, "foundry.toml")).exists(),
32
- Bun.file(join(cwd, "hardhat.config.js")).exists(),
33
- Bun.file(join(cwd, "hardhat.config.ts")).exists(),
34
- ];
35
-
36
- const results = await Promise.all(checks);
37
- return results.some(Boolean);
38
- }
39
-
40
- /**
41
- * Counts findings by severity from the audit state
42
- */
43
- function countFindingsBySeverity(
44
- findings: AuditState["findings"]
45
- ): Record<FindingSeverity, number> {
46
- const counts: Record<FindingSeverity, number> = {
47
- Critical: 0,
48
- High: 0,
49
- Medium: 0,
50
- Low: 0,
51
- Informational: 0,
52
- };
53
-
54
- for (const finding of findings) {
55
- counts[finding.severity]++;
56
- }
57
-
58
- return counts;
59
- }
60
-
61
- /**
62
- * Builds the audit state summary section for the injected context
63
- */
64
- function buildAuditStateSummary(state: AuditState | null): string {
65
- if (!state) {
66
- return "No active audit session. Use @argus to start an audit.";
67
- }
68
-
69
- const counts = countFindingsBySeverity(state.findings);
70
- const scopeList =
71
- state.scope.length > 0 ? state.scope.join(", ") : "not defined";
72
- const reviewedList =
73
- state.contractsReviewed.length > 0
74
- ? state.contractsReviewed.join(", ")
75
- : "none yet";
76
-
77
- return [
78
- `Phase: ${state.currentPhase}`,
79
- `Scope: ${scopeList}`,
80
- `Contracts reviewed: ${reviewedList}`,
81
- `Findings: ${state.findings.length} total — Critical: ${counts.Critical}, High: ${counts.High}, Medium: ${counts.Medium}, Low: ${counts.Low}, Info: ${counts.Informational}`,
82
- ].join("\n");
83
- }
84
-
85
- function collectSkillNamesFromRoot(rootPath: string): string[] {
86
- if (!existsSync(rootPath)) {
87
- return [];
88
- }
89
-
90
- const rootEntries = readdirSync(rootPath, { withFileTypes: true });
91
- const names = new Set<string>();
92
-
93
- for (const rootEntry of rootEntries) {
94
- if (!rootEntry.isDirectory()) continue;
95
-
96
- const directSkill = join(rootPath, rootEntry.name, "SKILL.md");
97
- if (existsSync(directSkill)) {
98
- names.add(rootEntry.name);
99
- continue;
100
- }
101
-
102
- const categoryDir = join(rootPath, rootEntry.name);
103
- const categoryEntries = readdirSync(categoryDir, { withFileTypes: true });
104
-
105
- for (const categoryEntry of categoryEntries) {
106
- if (!categoryEntry.isDirectory()) continue;
107
- const categorySkill = join(categoryDir, categoryEntry.name, "SKILL.md");
108
- if (existsSync(categorySkill)) {
109
- names.add(`${rootEntry.name}/${categoryEntry.name}`);
110
- }
111
- }
112
- }
113
-
114
- return Array.from(names).sort();
115
- }
116
-
117
- function getTrailOfBitsSkillRoots(): string[] {
118
- const pluginsDir = join(TOB_CACHE_DIR, "plugins");
119
- if (!existsSync(pluginsDir)) {
120
- return [];
121
- }
122
-
123
- const pluginEntries = readdirSync(pluginsDir, { withFileTypes: true });
124
- const roots: string[] = [];
125
-
126
- for (const pluginEntry of pluginEntries) {
127
- if (!pluginEntry.isDirectory()) continue;
128
- const pluginSkillsRoot = join(pluginsDir, pluginEntry.name, "skills");
129
- if (existsSync(pluginSkillsRoot)) {
130
- roots.push(pluginSkillsRoot);
131
- }
132
- }
133
-
134
- return roots;
135
- }
136
-
137
- function toSkillIndexEntry(skillNames: string[]): SkillIndexEntry {
138
- return {
139
- count: skillNames.length,
140
- sample: skillNames.slice(0, 3),
141
- };
142
- }
143
-
144
- function buildSkillIndexSnapshot(args: {
145
- argusConfig?: ArgusConfig;
146
- projectDir?: string;
147
- }): SkillIndexSnapshot {
148
- const bundledRoot = resolve(import.meta.dir, "../../skills");
149
- const bundledSkills = collectSkillNamesFromRoot(bundledRoot);
150
-
151
- const trailOfBitsSkills = new Set<string>();
152
- for (const tobRoot of getTrailOfBitsSkillRoots()) {
153
- for (const name of collectSkillNamesFromRoot(tobRoot)) {
154
- trailOfBitsSkills.add(name);
155
- }
156
- }
157
-
158
- const customDir = args.argusConfig?.knowledge?.customSkillsDir;
159
- const customRoot = customDir
160
- ? customDir.startsWith("/")
161
- ? customDir
162
- : resolve(args.projectDir ?? process.cwd(), customDir)
163
- : undefined;
164
- const customSkills = customRoot ? collectSkillNamesFromRoot(customRoot) : [];
165
-
166
- return {
167
- bundled: toSkillIndexEntry(bundledSkills),
168
- trailOfBits: toSkillIndexEntry(Array.from(trailOfBitsSkills).sort()),
169
- custom: toSkillIndexEntry(customSkills),
170
- };
171
- }
172
-
173
- function formatSkillSample(entry: SkillIndexEntry): string {
174
- return entry.sample.length > 0 ? entry.sample.join(", ") : "none";
175
- }
176
-
177
- /**
178
- * Builds the full audit context block to inject into the system prompt.
179
- * Designed to be concise (500-800 tokens).
180
- */
181
- function buildAuditContextBlock(
182
- state: AuditState | null,
183
- skillIndex: SkillIndexSnapshot
184
- ): string {
185
- return `
186
- <argus-context>
187
- ## Solidity Audit Context
188
-
189
- ### Severity Classification
190
- - **Critical**: Direct theft/freezing of funds, unauthorized admin access, contract destruction
191
- - **High**: Indirect fund loss, business logic manipulation, DoS on critical functions
192
- - **Medium**: Degraded functionality, edge-case bugs, partial DoS, poor validation
193
- - **Low**: Code quality issues, suboptimal patterns, missing events, minor logic issues
194
- - **Informational**: Gas optimizations, style suggestions, best practices, non-security notes
195
-
196
- ### Available Argus Tools
197
- - \`argus_slither_analyze\`: Run Slither static analysis on Solidity codebase
198
- - \`argus_forge_test\`: Execute Foundry/Forge tests for vulnerability verification
199
- - \`argus_forge_fuzz\`: Fuzz specific functions to discover edge cases
200
- - \`argus_analyze_contract\`: Generate deep structural profile of a contract
201
- - \`argus_check_patterns\`: Scan code against known vulnerability pattern library
202
- - \`argus_solodit_search\`: Search real-world audit reports and known vulnerabilities
203
- - \`argus_generate_report\`: Compile findings into structured audit report
204
- - \`argus_sync_knowledge\`: Update local vulnerability database (SCVD)
205
-
206
- ### Available Skills
207
- - \`vulnerability-patterns/*\`: 35+ exploit classes (reentrancy, oracle manipulation, flash loans)
208
- - \`protocol-patterns/*\`: AMM/DEX, lending, bridges, governance, staking-specific heuristics
209
- - \`methodology/*\`: audit workflow, severity calibration, and report structure guidance
210
- - \`checklists/*\`: structured review checklists for upgrades, integrations, and DeFi best practices
211
- - \`references/*\`: exploit references and vulnerable examples for historical precedent
212
- - Trail of Bits skills include both audit-relevant and general engineering skills; prioritize security-audit-oriented skills
213
-
214
- ### Skill Index Snapshot
215
- - Bundled skills: ${skillIndex.bundled.count} (examples: ${formatSkillSample(skillIndex.bundled)})
216
- - Trail of Bits skills: ${skillIndex.trailOfBits.count} (examples: ${formatSkillSample(skillIndex.trailOfBits)})
217
- - Custom project skills: ${skillIndex.custom.count} (examples: ${formatSkillSample(skillIndex.custom)})
218
- - Use the \`skill\` tool with exact skill names from the catalog
219
-
220
- ### Audit State
221
- ${buildAuditStateSummary(state)}
222
-
223
- ### Quick Reference
224
- Use @argus for full audits, @sentinel for testing, @pythia for research, @scribe for reports.
225
- Severity must follow classification above. Do not inflate severity.
226
- </argus-context>`.trim();
227
- }
228
-
229
- /**
230
- * Factory function that creates a system prompt transform hook.
231
- * The hook injects Solidity audit context when working in a Solidity project.
232
- *
233
- * @param getAuditState - Accessor function for current audit state (may return null)
234
- * @returns Async transform function compatible with OpenCode's experimental.chat.system.transform
235
- */
236
- export function createSystemPromptHook(
237
- getAuditState: () => AuditState | null,
238
- options?: { argusConfig?: ArgusConfig; projectDir?: string }
239
- ): (input: SystemPromptInput) => Promise<string | null> {
240
- const skillIndex = buildSkillIndexSnapshot({
241
- argusConfig: options?.argusConfig,
242
- projectDir: options?.projectDir,
243
- });
244
-
245
- return async (input: SystemPromptInput): Promise<string | null> => {
246
- const isSolidity = await isSolidityProject(input.cwd);
247
-
248
- if (!isSolidity) {
249
- return null;
250
- }
251
-
252
- const auditState = getAuditState();
253
- return buildAuditContextBlock(auditState, skillIndex);
254
- };
255
- }
256
-
257
- export default createSystemPromptHook;