solidity-argus 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +161 -1
  2. package/package.json +5 -2
  3. package/skills/README.md +63 -0
  4. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  5. package/skills/manifests/cyfrin.json +16 -0
  6. package/skills/manifests/defifofum.json +25 -0
  7. package/skills/manifests/kadenzipfel.json +48 -0
  8. package/skills/manifests/scvd.json +9 -0
  9. package/skills/manifests/smartbugs.json +11 -0
  10. package/skills/manifests/solodit.json +9 -0
  11. package/skills/manifests/sunweb3sec.json +11 -0
  12. package/skills/manifests/trailofbits.json +9 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  14. package/skills/patterns/access-control.yaml +31 -0
  15. package/skills/patterns/erc4626.yaml +29 -0
  16. package/skills/patterns/flash-loan.yaml +20 -0
  17. package/skills/patterns/oracle.yaml +30 -0
  18. package/skills/patterns/proxy.yaml +30 -0
  19. package/skills/patterns/reentrancy.yaml +30 -0
  20. package/skills/patterns/signature.yaml +31 -0
  21. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  22. package/skills/references/exploit-reference/SKILL.md +3 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
  24. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
  25. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
  26. package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
  27. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
  28. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
  29. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
  30. package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
  31. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
  32. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
  33. package/src/agents/argus-prompt.ts +7 -7
  34. package/src/agents/pythia-prompt.ts +11 -11
  35. package/src/agents/scribe-prompt.ts +6 -6
  36. package/src/agents/sentinel-prompt.ts +7 -7
  37. package/src/cli/cli-output.ts +16 -0
  38. package/src/cli/cli-program.ts +9 -5
  39. package/src/cli/commands/doctor.ts +274 -16
  40. package/src/cli/commands/init.ts +5 -5
  41. package/src/cli/commands/install.ts +5 -5
  42. package/src/cli/commands/lint-skills.ts +114 -0
  43. package/src/cli/tui-prompts.ts +4 -2
  44. package/src/config/schema.ts +2 -0
  45. package/src/create-hooks.ts +141 -32
  46. package/src/create-tools.ts +2 -0
  47. package/src/features/error-recovery/session-recovery.ts +7 -1
  48. package/src/features/error-recovery/tool-error-recovery.ts +74 -19
  49. package/src/features/persistent-state/audit-state-manager.ts +36 -13
  50. package/src/hooks/agent-tracker.ts +53 -0
  51. package/src/hooks/compaction-hook.ts +46 -37
  52. package/src/hooks/config-handler.ts +22 -9
  53. package/src/hooks/context-budget.ts +45 -0
  54. package/src/hooks/event-hook-v2.ts +8 -2
  55. package/src/hooks/event-hook.ts +5 -4
  56. package/src/hooks/knowledge-sync-hook.ts +2 -1
  57. package/src/hooks/recon-context-builder.ts +66 -0
  58. package/src/hooks/safe-create-hook.ts +4 -5
  59. package/src/hooks/system-prompt-hook.ts +92 -221
  60. package/src/hooks/tool-tracking-hook.ts +108 -9
  61. package/src/hooks/types.ts +0 -1
  62. package/src/index.ts +28 -6
  63. package/src/knowledge/retry.ts +53 -0
  64. package/src/knowledge/scvd-client.ts +37 -10
  65. package/src/knowledge/scvd-errors.ts +89 -0
  66. package/src/knowledge/scvd-index.ts +53 -3
  67. package/src/knowledge/scvd-sync.ts +205 -34
  68. package/src/knowledge/source-manifest.ts +102 -0
  69. package/src/plugin-interface.ts +11 -3
  70. package/src/shared/binary-utils.ts +1 -0
  71. package/src/shared/logger.ts +78 -17
  72. package/src/skills/argus-skill-resolver.ts +226 -0
  73. package/src/skills/skill-schema.ts +98 -0
  74. package/src/state/audit-state.ts +2 -0
  75. package/src/state/types.ts +32 -1
  76. package/src/tools/argus-skill-load-tool.ts +73 -0
  77. package/src/tools/pattern-checker-tool.ts +56 -12
  78. package/src/tools/pattern-loader.ts +183 -0
  79. package/src/tools/pattern-schema.ts +51 -0
  80. package/src/tools/report-generator-tool.ts +134 -11
  81. package/src/tools/slither-tool.ts +61 -19
  82. package/src/tools/solodit-search-tool.ts +92 -14
  83. package/src/utils/audit-artifact-detector.ts +119 -0
  84. package/src/utils/dependency-scanner.ts +93 -0
  85. package/src/utils/project-detector.ts +128 -26
  86. package/src/utils/solidity-parser.ts +20 -4
  87. package/src/utils/solodit-health.ts +29 -0
@@ -2,55 +2,154 @@ 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"
9
+ import { createAgentTracker } from "./hooks/agent-tracker"
10
+ import { createSystemPromptHook } from "./hooks/system-prompt-hook"
11
11
  import { safeCreateHook } from "./hooks/safe-create-hook"
12
+ import { createContextMonitor, createToolOutputTruncator } from "./features/context-monitor"
13
+ import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
14
+ import { getTokenBudgetForAgent } from "./hooks/context-budget"
15
+ import { createSessionRecoveryHandler } from "./features/error-recovery"
16
+ import { createToolErrorRecoveryHandler } from "./features/error-recovery"
17
+ import { detectProject } from "./utils/project-detector"
18
+ import type { ProjectConfig } from "./utils/project-detector"
19
+ import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
20
+ import type { ReconContext } from "./hooks/recon-context-builder"
21
+ import { buildReconContextBlock } from "./hooks/recon-context-builder"
22
+ import type { AuditState } from "./state/types"
23
+
24
+ let latestAgentTracker: ReturnType<typeof createAgentTracker> | undefined
25
+
26
+ export function getAgentForSession(sessionID: string): string | undefined {
27
+ return latestAgentTracker?.getAgentForSession(sessionID)
28
+ }
29
+
30
+ export function isArgusAgent(sessionID: string): boolean {
31
+ return latestAgentTracker?.isArgusAgent(sessionID) ?? false
32
+ }
12
33
 
13
34
  export type Hooks = Pick<
14
35
  PluginHooks,
15
36
  | "config"
37
+ | "chat.params"
38
+ | "chat.message"
16
39
  | "experimental.chat.system.transform"
17
40
  | "experimental.session.compacting"
18
41
  | "tool.execute.after"
19
42
  | "event"
20
43
  >
21
44
 
45
+ /**
46
+ * Creates the hook handlers for the Argus plugin.
47
+ *
48
+ * Context Delivery Strategy:
49
+ * - Prompt: Static agent identity (src/agents/*-prompt.ts) — methodology, personality, tool instructions
50
+ * - Hook: Dynamic state injection via experimental.chat.system.transform — audit progress, findings, phase
51
+ * - Skill-load: On-demand knowledge via argus_skill_load tool — vulnerability patterns, protocol knowledge
52
+ *
53
+ * The system.transform hook injects dynamic audit context only for Argus-family agents
54
+ * (argus, sentinel, pythia, scribe). Non-audit agents receive no injection.
55
+ */
22
56
  export function createHooks(args: {
23
57
  config: ArgusConfig
24
58
  managers: Managers
25
59
  projectDir: string
26
60
  isHookEnabled: (name: HookName) => boolean
27
61
  }): Hooks {
28
- const { config, projectDir, isHookEnabled } = args
29
-
30
- const { state: auditState, store: findingStore } = createAuditState(projectDir)
31
- const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir)
32
- setAuditState(auditState)
33
-
34
- const systemPromptHook = isHookEnabled("system-prompt")
35
- ? safeCreateHook(
36
- () =>
37
- createSystemPromptHook(getAuditState, {
38
- argusConfig: config,
39
- projectDir,
40
- }),
41
- "system-prompt"
42
- )
43
- : undefined
62
+ const { config, managers, projectDir, isHookEnabled } = args
63
+ const { auditStateManager, backgroundManager } = managers
64
+ const agentTracker = createAgentTracker()
65
+ latestAgentTracker = agentTracker
66
+
67
+ const contextMonitor = createContextMonitor()
68
+ const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
69
+ let auditStateGetter: (() => AuditState | null) | undefined
70
+ const toolErrorRecoveryHandler = createToolErrorRecoveryHandler(() => auditStateGetter?.() ?? null)
71
+ const outputTruncator = createToolOutputTruncator()
72
+
73
+ const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
74
+ async ({ type, sessionId, auditState, setAuditState: setState }) => {
75
+ if (type === "session.created") {
76
+ const recoveredState = await auditStateManager.load()
77
+ if (recoveredState) {
78
+ setState(recoveredState)
79
+ }
80
+ return
81
+ }
82
+
83
+ if (type === "session.idle" && auditState) {
84
+ await auditStateManager.save(auditState)
85
+ return
86
+ }
87
+
88
+ if (type === "session.deleted") {
89
+ if (sessionId) {
90
+ agentTracker.clearSession(sessionId)
91
+ }
92
+ await auditStateManager.reset()
93
+ }
94
+ },
95
+ async ({ type, sessionId, setAuditState: setState }) => {
96
+ await sessionRecoveryHandler({ type, sessionId, setAuditState: setState })
97
+ },
98
+ async ({ type }) => {
99
+ if (type === "session.idle") {
100
+ backgroundManager.getActiveCount()
101
+ }
102
+ },
103
+ ])
104
+
105
+ auditStateGetter = getAuditState
106
+
107
+ const initialState = auditStateManager.get()
108
+ if (initialState) {
109
+ setAuditState(initialState)
110
+ }
111
+
112
+ const auditEnforcer = createAuditEnforcer()
113
+
114
+ const systemPromptHook = createSystemPromptHook({
115
+ getAuditState,
116
+ getAgentForSession: agentTracker.getAgentForSession,
117
+ isArgusAgent: agentTracker.isArgusAgent,
118
+ getContextPressure: (systemText: string) => {
119
+ const status = contextMonitor.getContextStatus(systemText, getAuditState())
120
+ return status.usage
121
+ },
122
+ getTokenBudget: getTokenBudgetForAgent,
123
+ getEnforcerReminder: auditEnforcer,
124
+ getReconBlock: () => buildReconContextBlock({
125
+ projectConfig: reconProjectConfig,
126
+ dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
127
+ auditArtifacts: detectAuditArtifacts(projectDir),
128
+ }),
129
+ })
130
+
131
+ let reconProjectConfig: ProjectConfig | null = null
132
+
133
+ detectProject(projectDir)
134
+ .then((config) => {
135
+ reconProjectConfig = config
136
+ })
137
+ .catch(() => {
138
+ // Silent fallback — audit artifacts remain available
139
+ })
140
+
141
+ const getReconContext = (): ReconContext => ({
142
+ projectConfig: reconProjectConfig,
143
+ dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
144
+ auditArtifacts: detectAuditArtifacts(projectDir),
145
+ })
44
146
 
45
147
  const compactionHook = isHookEnabled("compaction")
46
- ? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
148
+ ? safeCreateHook(() => createCompactionHook(getAuditState, getReconContext), "compaction")
47
149
  : undefined
48
150
 
49
151
  const toolTrackingHook = isHookEnabled("tool-tracking")
50
- ? safeCreateHook(
51
- () => createToolTrackingHook(auditState, findingStore),
52
- "tool-tracking"
53
- )
152
+ ? safeCreateHook(() => createToolTrackingHook(getAuditState), "tool-tracking")
54
153
  : undefined
55
154
 
56
155
  const safeEventHook = isHookEnabled("event")
@@ -59,15 +158,15 @@ export function createHooks(args: {
59
158
 
60
159
  return {
61
160
  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,
161
+ "chat.params": async (input) => {
162
+ agentTracker.chatParamsHook(input)
163
+ },
164
+ "chat.message": async (input) => {
165
+ agentTracker.chatMessageHook(input)
166
+ },
167
+ "experimental.chat.system.transform": async (input, output) => {
168
+ await systemPromptHook(input, output)
169
+ },
71
170
  "experimental.session.compacting": compactionHook
72
171
  ? async (_input, output) => {
73
172
  const block = await compactionHook({ summary: output.context.join("\n") })
@@ -76,11 +175,21 @@ export function createHooks(args: {
76
175
  : undefined,
77
176
  "tool.execute.after": toolTrackingHook
78
177
  ? async (input, output) => {
178
+ const recoveryHint = toolErrorRecoveryHandler({
179
+ tool: input.tool,
180
+ result: output.output,
181
+ })
182
+
79
183
  await toolTrackingHook({
80
184
  tool: input.tool,
81
185
  args: input.args,
82
186
  result: output.output,
83
187
  })
188
+
189
+ const outputWithHint = recoveryHint
190
+ ? `${output.output}${recoveryHint}`
191
+ : output.output
192
+ output.output = outputTruncator(outputWithHint)
84
193
  }
85
194
  : undefined,
86
195
  event: safeEventHook,
@@ -8,6 +8,7 @@ import { patternCheckerTool } from "./tools/pattern-checker-tool"
8
8
  import { soloditSearchTool } from "./tools/solodit-search-tool"
9
9
  import { reportGeneratorTool } from "./tools/report-generator-tool"
10
10
  import { syncKnowledgeTool } from "./tools/sync-knowledge-tool"
11
+ import { argusSkillLoadTool } from "./tools/argus-skill-load-tool"
11
12
 
12
13
  export function createTools(
13
14
  config: ArgusConfig,
@@ -18,6 +19,7 @@ export function createTools(
18
19
  argus_forge_fuzz: forgeFuzzTool,
19
20
  argus_analyze_contract: contractAnalyzerTool,
20
21
  argus_check_patterns: patternCheckerTool,
22
+ argus_skill_load: argusSkillLoadTool,
21
23
  argus_generate_report: reportGeneratorTool,
22
24
  argus_sync_knowledge: syncKnowledgeTool,
23
25
  }
@@ -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,15 +1,60 @@
1
1
  import { createLogger } from "../../shared/logger"
2
+ import type { AuditState } from "../../state/types"
2
3
 
3
- const RECOVERY_HINTS: Record<string, string> = {
4
- slither: "Install Slither: pip install slither-analyzer",
5
- forge: "Install Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup",
6
- solodit: "Check network connectivity or Solodit API status",
7
- scvd: "Check SCVD API at https://api.scvd.dev — may be temporarily unavailable",
4
+ type ToolFallbackEntry = {
5
+ install: string
6
+ fallback: string
8
7
  }
9
8
 
10
- const VIA_IR_HINT = "Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
9
+ const TOOL_FALLBACKS: Record<string, ToolFallbackEntry> = {
10
+ slither: {
11
+ install: "pip install slither-analyzer",
12
+ fallback:
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
+ },
15
+ forge: {
16
+ install: "curl -L https://foundry.paradigm.xyz | bash && foundryup",
17
+ fallback:
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
+ },
20
+ solodit: {
21
+ install: "",
22
+ fallback:
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
+ },
25
+ scvd: {
26
+ install: "",
27
+ fallback:
28
+ "SCVD API is unavailable. PROCEED with local patterns and Solodit search if available.",
29
+ },
30
+ }
31
+
32
+ const VIA_IR_HINT =
33
+ "Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
34
+
35
+ function isToolUnavailable(lowerResult: string): boolean {
36
+ return (
37
+ lowerResult.includes("enoent") ||
38
+ lowerResult.includes("not found") ||
39
+ lowerResult.includes("not installed")
40
+ )
41
+ }
42
+
43
+ function isToolError(lowerResult: string): boolean {
44
+ return (
45
+ isToolUnavailable(lowerResult) ||
46
+ lowerResult.includes("command failed") ||
47
+ lowerResult.includes("error:")
48
+ )
49
+ }
50
+
51
+ function resolveToolBase(tool: string): string {
52
+ return tool.replace("argus_", "").split("_")[0] ?? ""
53
+ }
11
54
 
12
- export function createToolErrorRecoveryHandler() {
55
+ export function createToolErrorRecoveryHandler(
56
+ getAuditState?: () => AuditState | null,
57
+ ) {
13
58
  const logger = createLogger()
14
59
 
15
60
  return (toolResult: { tool: string; result: string }): string | null => {
@@ -27,22 +72,32 @@ export function createToolErrorRecoveryHandler() {
27
72
  return `\n[Argus Recovery Hint] ${VIA_IR_HINT}`
28
73
  }
29
74
 
30
- const isError =
31
- lowerResult.includes("enoent") ||
32
- lowerResult.includes("not found") ||
33
- lowerResult.includes("command failed") ||
34
- lowerResult.includes("error:")
75
+ if (!isToolError(lowerResult)) return null
35
76
 
36
- if (!isError) return null
77
+ const toolBase = resolveToolBase(tool)
78
+ const entry = TOOL_FALLBACKS[toolBase]
79
+ if (!entry) return null
37
80
 
38
- const toolBase = tool.replace("argus_", "").split("_")[0] ?? ""
39
- const hint = RECOVERY_HINTS[toolBase]
81
+ const unavailable = isToolUnavailable(lowerResult)
82
+
83
+ if (unavailable && getAuditState) {
84
+ const state = getAuditState()
85
+ if (state) {
86
+ state.unavailableTools ??= []
87
+ if (!state.unavailableTools.includes(toolBase)) {
88
+ state.unavailableTools.push(toolBase)
89
+ logger.info(`Recorded ${toolBase} as unavailable — fallback activated`)
90
+ }
91
+ }
92
+ }
40
93
 
41
- if (hint) {
42
- logger.info(`Tool error recovery hint for ${tool}: ${hint}`)
43
- return `\n[Argus Recovery Hint] ${hint}`
94
+ if (unavailable) {
95
+ logger.info(`Tool unavailable fallback for ${tool}`)
96
+ return `\n[Argus Fallback] ${entry.fallback}`
44
97
  }
45
98
 
46
- return null
99
+ const installHint = entry.install ? ` (install: ${entry.install})` : ""
100
+ logger.info(`Tool error recovery hint for ${tool}`)
101
+ return `\n[Argus Recovery Hint] ${toolBase} error${installHint}. ${entry.fallback}`
47
102
  }
48
103
  }
@@ -7,7 +7,7 @@ import { createLogger } from "../../shared/logger";
7
7
 
8
8
  const STATE_FILE_DIR = ".opencode";
9
9
  const STATE_FILE_NAME = "argus-state.json";
10
- const STATE_VERSION = "1";
10
+ const STATE_VERSION = "2";
11
11
 
12
12
  function isObject(value: unknown): value is Record<string, unknown> {
13
13
  return typeof value === "object" && value !== null;
@@ -39,9 +39,11 @@ function isPersistentAuditState(value: unknown): value is PersistentAuditState {
39
39
  return false;
40
40
  }
41
41
 
42
+ const hasSupportedVersion = value.version === "1" || value.version === "2";
43
+
42
44
  return (
43
45
  typeof value.savedAt === "number" &&
44
- typeof value.version === "string" &&
46
+ hasSupportedVersion &&
45
47
  typeof value.filePath === "string"
46
48
  );
47
49
  }
@@ -69,7 +71,17 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
69
71
  return null;
70
72
  }
71
73
 
72
- const { savedAt: _savedAt, version: _version, filePath: _filePath, ...state } = parsed;
74
+ const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed;
75
+
76
+ if (version === "1") {
77
+ if (!state.soloditResults) {
78
+ state.soloditResults = [];
79
+ }
80
+ if (!state.fuzzCounterexamples) {
81
+ state.fuzzCounterexamples = [];
82
+ }
83
+ }
84
+
73
85
  currentState = state;
74
86
  return currentState;
75
87
  } catch (_error) {
@@ -77,20 +89,31 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
77
89
  }
78
90
  }
79
91
 
92
+ let saveInFlight = false;
93
+
80
94
  async function save(state: AuditState): Promise<void> {
81
95
  currentState = state;
82
96
 
83
- const persistentState: PersistentAuditState = {
84
- ...state,
85
- savedAt: Date.now(),
86
- version: STATE_VERSION,
87
- filePath: stateFilePath,
88
- };
97
+ if (saveInFlight) return;
98
+ saveInFlight = true;
89
99
 
90
- const tempFilePath = `${stateFilePath}.tmp`;
91
- await mkdir(dirname(stateFilePath), { recursive: true });
92
- await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
93
- await rename(tempFilePath, stateFilePath);
100
+ try {
101
+ const persistentState: PersistentAuditState = {
102
+ ...state,
103
+ savedAt: Date.now(),
104
+ version: STATE_VERSION,
105
+ filePath: stateFilePath,
106
+ };
107
+
108
+ const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`;
109
+ await mkdir(dirname(stateFilePath), { recursive: true });
110
+ await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
111
+ await rename(tempFilePath, stateFilePath);
112
+ } catch {
113
+ // Non-critical: state persistence is best-effort
114
+ } finally {
115
+ saveInFlight = false;
116
+ }
94
117
  }
95
118
 
96
119
  function get(): AuditState {
@@ -0,0 +1,53 @@
1
+ import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
2
+
3
+ type ChatParamsInput = Parameters<NonNullable<PluginHooks["chat.params"]>>[0] & {
4
+ agent?: string
5
+ }
6
+ type ChatMessageInput = Parameters<NonNullable<PluginHooks["chat.message"]>>[0]
7
+
8
+ const ARGUS_FAMILY = new Set(["argus", "sentinel", "pythia", "scribe"])
9
+
10
+ export type AgentTracker = ReturnType<typeof createAgentTracker>
11
+
12
+ export function createAgentTracker() {
13
+ const sessions = new Map<string, string>()
14
+
15
+ const trackSession = (sessionID: string, agent?: string): void => {
16
+ if (!agent) {
17
+ return
18
+ }
19
+
20
+ sessions.set(sessionID, agent)
21
+ }
22
+
23
+ return {
24
+ chatParamsHook: (input: ChatParamsInput): void => {
25
+ trackSession(input.sessionID, input.agent)
26
+ },
27
+
28
+ chatMessageHook: (input: ChatMessageInput): void => {
29
+ trackSession(input.sessionID, input.agent)
30
+ },
31
+
32
+ getAgentForSession: (sessionID: string): string | undefined => {
33
+ return sessions.get(sessionID)
34
+ },
35
+
36
+ isArgusAgent: (sessionID: string): boolean => {
37
+ const agent = sessions.get(sessionID)
38
+ if (!agent) {
39
+ return false
40
+ }
41
+
42
+ return ARGUS_FAMILY.has(agent)
43
+ },
44
+
45
+ clearSession: (sessionID: string): void => {
46
+ sessions.delete(sessionID)
47
+ },
48
+
49
+ getTrackedSessions: (): Map<string, string> => {
50
+ return sessions
51
+ },
52
+ }
53
+ }
@@ -1,50 +1,59 @@
1
1
  import type { AuditState, FindingSeverity } from "../state/types"
2
+ import type { ReconContext } from "./recon-context-builder"
3
+ import { buildReconContextBlock } from "./recon-context-builder"
2
4
 
3
- /**
4
- * Creates a compaction hook that serializes audit state into XML format
5
- * so findings survive context window compression.
6
- *
7
- * The returned hook is called by OpenCode's `experimental.session.compacting`
8
- * event, receiving `{ summary: string }` and returning the enriched summary.
9
- */
10
5
  export function createCompactionHook(
11
- getAuditState: () => AuditState | null
6
+ getAuditState: () => AuditState | null,
7
+ getReconContext?: () => ReconContext | null,
12
8
  ): (input: { summary: string }) => Promise<string | null> {
13
9
  return async (_input: { summary: string }): Promise<string | null> => {
14
10
  const state = getAuditState()
15
- if (!state) {
16
- return null
17
- }
18
11
 
19
- const severityCounts: Record<FindingSeverity, number> = {
20
- Critical: 0,
21
- High: 0,
22
- Medium: 0,
23
- Low: 0,
24
- Informational: 0,
12
+ const parts: string[] = []
13
+
14
+ 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
+ }
26
+
27
+ const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
28
+ const contracts = state.contractsReviewed.join(", ")
29
+ const started = new Date(state.startTime).toISOString()
30
+
31
+ parts.push(
32
+ [
33
+ "<argus-audit-state>",
34
+ `Phase: ${state.currentPhase}`,
35
+ `Contracts Reviewed: ${contracts}`,
36
+ "Findings:",
37
+ ` Critical: ${severityCounts.Critical}`,
38
+ ` High: ${severityCounts.High}`,
39
+ ` Medium: ${severityCounts.Medium}`,
40
+ ` Low: ${severityCounts.Low}`,
41
+ ` Informational: ${severityCounts.Informational}`,
42
+ `Tools Executed: ${toolNames}`,
43
+ `Started: ${started}`,
44
+ "</argus-audit-state>",
45
+ ].join("\n"),
46
+ )
25
47
  }
26
48
 
27
- for (const finding of state.findings) {
28
- severityCounts[finding.severity]++
49
+ if (getReconContext) {
50
+ const recon = getReconContext()
51
+ if (recon) {
52
+ const reconBlock = buildReconContextBlock(recon)
53
+ if (reconBlock) parts.push(reconBlock)
54
+ }
29
55
  }
30
56
 
31
- const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
32
- const contracts = state.contractsReviewed.join(", ")
33
- const started = new Date(state.startTime).toISOString()
34
-
35
- return [
36
- "<argus-audit-state>",
37
- `Phase: ${state.currentPhase}`,
38
- `Contracts Reviewed: ${contracts}`,
39
- "Findings:",
40
- ` Critical: ${severityCounts.Critical}`,
41
- ` High: ${severityCounts.High}`,
42
- ` Medium: ${severityCounts.Medium}`,
43
- ` Low: ${severityCounts.Low}`,
44
- ` Informational: ${severityCounts.Informational}`,
45
- `Tools Executed: ${toolNames}`,
46
- `Started: ${started}`,
47
- "</argus-audit-state>",
48
- ].join("\n")
57
+ return parts.length > 0 ? parts.join("\n") : null
49
58
  }
50
59
  }
@@ -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(
@@ -83,6 +93,7 @@ export function createConfigHandler(
83
93
  argus_forge_fuzz: "allow",
84
94
  argus_analyze_contract: "allow",
85
95
  argus_check_patterns: "allow",
96
+ argus_skill_load: "allow",
86
97
  skill: "allow",
87
98
  },
88
99
  },
@@ -94,6 +105,7 @@ export function createConfigHandler(
94
105
  permission: {
95
106
  argus_solodit_search: "allow",
96
107
  argus_check_patterns: "allow",
108
+ argus_skill_load: "allow",
97
109
  skill: "allow",
98
110
  },
99
111
  },
@@ -104,6 +116,7 @@ export function createConfigHandler(
104
116
  prompt: SCRIBE_PROMPT,
105
117
  permission: {
106
118
  argus_generate_report: "allow",
119
+ argus_skill_load: "allow",
107
120
  skill: "allow",
108
121
  },
109
122
  },