solidity-argus 0.3.4 → 0.3.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.
- package/package.json +4 -4
- package/src/agents/argus-prompt.ts +56 -2
- package/src/agents/pythia-prompt.ts +11 -0
- package/src/agents/scribe-prompt.ts +9 -4
- package/src/agents/sentinel-prompt.ts +10 -0
- package/src/cli/commands/init.ts +1 -1
- package/src/config/schema.ts +2 -2
- package/src/create-hooks.ts +95 -12
- package/src/create-tools.ts +2 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +30 -2
- package/src/features/persistent-state/audit-state-manager.ts +180 -10
- package/src/features/persistent-state/event-sink.ts +15 -6
- package/src/features/persistent-state/findings-materializer.ts +52 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-finalizer.ts +26 -7
- package/src/features/persistent-state/run-journal.ts +12 -4
- package/src/hooks/event-hook.ts +4 -1
- package/src/hooks/system-prompt-hook.ts +15 -0
- package/src/hooks/tool-tracking-hook.ts +168 -10
- package/src/shared/audit-artifact-resolver.ts +13 -12
- package/src/shared/file-utils.ts +7 -2
- package/src/shared/index.ts +8 -8
- package/src/shared/path-root-resolver.ts +34 -0
- package/src/shared/plugin-metadata.ts +23 -0
- package/src/shared/report-path-resolver.ts +3 -3
- package/src/state/adapters.ts +99 -5
- package/src/state/finding-aggregation.ts +100 -0
- package/src/state/finding-fingerprint.ts +47 -0
- package/src/state/finding-store.ts +19 -29
- package/src/state/projectors.ts +18 -4
- package/src/state/schemas.ts +145 -1
- package/src/state/types.ts +17 -1
- package/src/tools/record-finding-tool.ts +125 -0
- package/src/tools/report-generator-tool.ts +116 -7
- package/src/tools/report-preflight.ts +79 -0
- package/src/tools/solodit-search-tool.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solidity-argus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Solidity smart contract security auditing plugin for OpenCode — 4 specialized agents, 12 tools (11 core + optional Solodit), and a curated vulnerability knowledge base",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solidity",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
|
-
"solidity-argus": "
|
|
29
|
-
"argus": "
|
|
28
|
+
"solidity-argus": "src/cli/index.ts",
|
|
29
|
+
"argus": "src/cli/index.ts"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
|
32
32
|
"src/",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"repository": {
|
|
68
68
|
"type": "git",
|
|
69
|
-
"url": "https://github.com/Apegurus/solidity-argus"
|
|
69
|
+
"url": "git+https://github.com/Apegurus/solidity-argus.git"
|
|
70
70
|
},
|
|
71
71
|
"engines": {
|
|
72
72
|
"bun": ">=1.0.0"
|
|
@@ -225,6 +225,52 @@ Task(subagent_type="scribe", prompt="Generate the final audit report for Project
|
|
|
225
225
|
\`\`\`
|
|
226
226
|
- Wait for both to complete before synthesizing their results.
|
|
227
227
|
|
|
228
|
+
### STATE-FIRST SYNTHESIS POLICY
|
|
229
|
+
|
|
230
|
+
**Synthesize and report from durable evidence — not transcript tails.**
|
|
231
|
+
|
|
232
|
+
When building the final report or synthesizing findings:
|
|
233
|
+
1. **Primary source**: \`toolsExecuted\` records, \`findings\` from state, and event stream data persisted via argus_* tool outputs.
|
|
234
|
+
2. **Secondary source**: Tool transcript text (use only when durable evidence is unavailable or incomplete).
|
|
235
|
+
3. **Never** synthesize findings from ephemeral background transcript retrieval alone if durable state evidence exists.
|
|
236
|
+
4. **Manual-finding durability**: If Argus, Sentinel, or Pythia identifies a finding outside analyzer tool payloads, they must call \
|
|
237
|
+
\`argus_record_finding\` before proceeding.
|
|
238
|
+
5. **Report parity rule**: Scribe must not include findings in \`report_input\` unless they are event-backed (recorded via tools/events).
|
|
239
|
+
|
|
240
|
+
**Bounded background fan-out**: For deep audits, limit concurrent high-context background delegations to max 2 at a time. Split larger workloads into sequential waves. This prevents retrieval blind spots from simultaneous long-running tasks.
|
|
241
|
+
|
|
242
|
+
Example — correct fan-out:
|
|
243
|
+
- Wave 1: [Sentinel: slither + pattern check] + [Pythia: solodit search] (2 background tasks)
|
|
244
|
+
- Wait for both. Then Wave 2: [Sentinel: forge tests] (1 background task)
|
|
245
|
+
|
|
246
|
+
## SYNTHESIS BARRIER: MUST NOT PROCEED WITHOUT DURABLE EVIDENCE
|
|
247
|
+
|
|
248
|
+
You **must not proceed** to synthesis or report generation until required durable evidence is confirmed present:
|
|
249
|
+
- \`toolsExecuted\` records exist for all planned tools
|
|
250
|
+
- Expected findings coverage is populated in state
|
|
251
|
+
- Lifecycle invariants are satisfied (no orphaned tool starts)
|
|
252
|
+
|
|
253
|
+
### Adaptive Retrieval Budget
|
|
254
|
+
|
|
255
|
+
When waiting for background tasks, use bounded retrieval budgets by workload class:
|
|
256
|
+
|
|
257
|
+
| Class | Budget | Criteria |
|
|
258
|
+
|----------|---------|---------------------------------------------|
|
|
259
|
+
| quick | 60s | Single-tool or single-contract checks |
|
|
260
|
+
| standard | 180s | Multi-tool single-agent batches |
|
|
261
|
+
| deep | 600s | Multi-agent or synthesis-heavy runs |
|
|
262
|
+
|
|
263
|
+
Poll until the task reaches a terminal state: \`completed\`, \`error\`, \`cancelled\`, or \`interrupt\`.
|
|
264
|
+
|
|
265
|
+
### Re-dispatch (LAST RESORT)
|
|
266
|
+
|
|
267
|
+
Re-dispatch is only justified when ALL of these are true:
|
|
268
|
+
1. The task has reached terminal state OR retrieval budget has expired
|
|
269
|
+
2. Required durable evidence is STILL missing from state/events
|
|
270
|
+
3. The gap is specific and bounded (not a general "redo everything")
|
|
271
|
+
|
|
272
|
+
**When re-dispatching**: Target only missing evidence segments. Use \`run_in_background=false\` (foreground only) for re-dispatch pivots. Do NOT re-dispatch routinely after a single transcript retrieval miss if durable state evidence is already complete.
|
|
273
|
+
|
|
228
274
|
## TASK COMPLETION TRACKING
|
|
229
275
|
|
|
230
276
|
You must track which audit phases are complete to avoid redundant work and tool re-execution.
|
|
@@ -272,7 +318,12 @@ Your subagents have access to these specialized tools. Know when to delegate eac
|
|
|
272
318
|
- **\`argus_generate_report\`**:
|
|
273
319
|
- **Use**: During Reporting.
|
|
274
320
|
- **Purpose**: Generates the final artifact.
|
|
275
|
-
- **Note**: Requires a versioned report_input JSON string matching the ReportInput contract (schema_version
|
|
321
|
+
- **Note**: Requires a versioned report_input JSON string matching the ReportInput contract (schema_version 2.0.0). Do not send natural-language-only findings to Scribe for tool invocation.
|
|
322
|
+
|
|
323
|
+
- **\`argus_record_finding\`**:
|
|
324
|
+
- **Use**: Whenever a manual/non-tool finding is identified.
|
|
325
|
+
- **Purpose**: Persist manually identified findings as canonical event-backed observations before reporting.
|
|
326
|
+
- **Note**: Accepts a single finding or an array. Call it immediately when the finding is identified.
|
|
276
327
|
|
|
277
328
|
- **\`argus_sync_knowledge\`**:
|
|
278
329
|
- **Use**: Maintenance.
|
|
@@ -424,6 +475,8 @@ Tools may fail. You must be resilient.
|
|
|
424
475
|
|
|
425
476
|
After you have synthesized your findings, build a canonical ReportInput payload and invoke Scribe:
|
|
426
477
|
|
|
478
|
+
**State-first requirement**: Before invoking Scribe, verify that \`toolsExecuted\` in your ReportInput contains entries for each tool you ran. Do NOT proceed to report generation if required tool coverage is missing from durable state — re-run the missing tool instead. Use \`preflight_policy: "strict-fail"\` for the final report invocation.
|
|
479
|
+
|
|
427
480
|
\`\`\`
|
|
428
481
|
Task(subagent_type="scribe", prompt="Generate the final security audit report.
|
|
429
482
|
|
|
@@ -436,7 +489,7 @@ ReportInput JSON (pass EXACTLY, no prose substitution):
|
|
|
436
489
|
"session_id": "{session-id}",
|
|
437
490
|
"tool_call_id": "{tool-call-id}",
|
|
438
491
|
"source": "argus",
|
|
439
|
-
"schema_version": "
|
|
492
|
+
"schema_version": "2.0.0",
|
|
440
493
|
"projectDir": "{project-dir}",
|
|
441
494
|
"findings": [canonical findings],
|
|
442
495
|
"toolsExecuted": [canonical tool executions],
|
|
@@ -454,6 +507,7 @@ Scribe must call argus_generate_report with:
|
|
|
454
507
|
- project_name: project name
|
|
455
508
|
- scope: audited file list
|
|
456
509
|
- report_input: serialized ReportInput JSON string
|
|
510
|
+
- preflight_policy: "strict-fail" (non-negotiable for final report)
|
|
457
511
|
|
|
458
512
|
Legacy audit_state is transitional-only and deprecated.
|
|
459
513
|
|
|
@@ -49,6 +49,7 @@ You must follow this structured research process:
|
|
|
49
49
|
### 4. Reporting
|
|
50
50
|
- **Objective**: Deliver actionable intelligence to Argus.
|
|
51
51
|
- **Actions**:
|
|
52
|
+
- If you identify a manual finding from precedent/pattern reasoning, call \`argus_record_finding\` before reporting back.
|
|
52
53
|
- Format findings clearly, citing the precedent (e.g., "Similar to the Cream Finance hack").
|
|
53
54
|
- Assess severity based on the *likelihood* of exploitation in this specific context.
|
|
54
55
|
|
|
@@ -84,6 +85,16 @@ You have two primary tools. Master them.
|
|
|
84
85
|
- Returns a list of matches with line numbers.
|
|
85
86
|
- **Crucial**: You must verify the context. A regex match for \`selfdestruct\` is not a bug if it's in a test file or a legitimate upgrade mechanism (though still risky).
|
|
86
87
|
|
|
88
|
+
### 3. \`argus_record_finding\`
|
|
89
|
+
**Purpose**: Persist research/manual findings into durable event-backed observations.
|
|
90
|
+
**When to use**:
|
|
91
|
+
- Whenever your finding is derived from precedent analysis or manual reasoning rather than a direct analyzer payload.
|
|
92
|
+
**Arguments**:
|
|
93
|
+
- \`finding\` (string): Serialized JSON object for one finding.
|
|
94
|
+
- \`findings\` (string): Serialized JSON array for multiple findings.
|
|
95
|
+
**Interpretation**:
|
|
96
|
+
- A finding is not report-ready until it has been recorded through this tool.
|
|
97
|
+
|
|
87
98
|
## EMPTY RESULTS STRATEGY
|
|
88
99
|
|
|
89
100
|
When \`argus_solodit_search\` returns zero results for a query:
|
|
@@ -44,10 +44,15 @@ You must adhere to these strict writing standards:
|
|
|
44
44
|
Argus passes you structured report data. Use that payload directly and keep it schema-accurate.
|
|
45
45
|
|
|
46
46
|
**Your workflow**:
|
|
47
|
-
1. Validate Argus provided a serialized ReportInput JSON string (schema_version
|
|
48
|
-
2.
|
|
49
|
-
3.
|
|
50
|
-
4.
|
|
47
|
+
1. Validate Argus provided a serialized ReportInput JSON string (schema_version 2.0.0) with required fields: run_id, seq, session_id, tool_call_id, source, schema_version, projectDir, findings, toolsExecuted, scope. **Execution integrity check**: \`toolsExecuted\` must be non-empty for the audit to be considered complete. If \`toolsExecuted\` is empty or missing key tool families (slither, forge, patterns), add a \`## Limitations\` section to the report noting which tool coverage is absent.
|
|
48
|
+
2. Enforce parity: do not include findings unless they are event-backed observations (recorded through tool/event flow, including \`argus_record_finding\`).
|
|
49
|
+
3. Write the complete report in Markdown following the Report Structure and Output Format sections.
|
|
50
|
+
4. Call \`argus_generate_report\` with arguments { project_name, scope, report_input }. Use legacy \`audit_state\` only for transitional compatibility and treat it as deprecated.
|
|
51
|
+
5. **Limitations disclosure** (MANDATORY when tools fail): If any tool was unavailable, timed out, or failed, add a \`## Limitations\` section to the report BEFORE \`## Findings\`. Use this format:
|
|
52
|
+
- \`**Tool name**: [reason \u2014 unavailable/failed/timed out]. [Impact on finding coverage if any.]\`
|
|
53
|
+
- Example: \`**argus_solodit_search**: External database was unavailable. Known-vulnerability cross-referencing was performed using local patterns only.\`
|
|
54
|
+
- Never silently omit limitations — incomplete coverage must be disclosed.
|
|
55
|
+
6. Confirm the report was generated in your response to Argus: "Report generated via argus_generate_report: {filePath}".
|
|
51
56
|
|
|
52
57
|
## SINGLE-WRITER POLICY
|
|
53
58
|
|
|
@@ -32,6 +32,7 @@ You operate in a loop of **Scan -> Analyze -> Verify**.
|
|
|
32
32
|
|
|
33
33
|
4. **Reporting**:
|
|
34
34
|
- Format your findings strictly according to the Output Format section.
|
|
35
|
+
- If you identify a manual finding outside analyzer payloads, call \`argus_record_finding\` immediately.
|
|
35
36
|
- Report back to Argus with confirmed findings.
|
|
36
37
|
|
|
37
38
|
## POC VERIFICATION
|
|
@@ -127,6 +128,15 @@ You have access to a specific set of tools. Use them effectively.
|
|
|
127
128
|
- High gas consumption often correlates with complex logic, unbounded loops, or storage-heavy operations.
|
|
128
129
|
- Gas hotspots are prime candidates for DoS vulnerabilities.
|
|
129
130
|
|
|
131
|
+
### 9. \`argus_record_finding\`
|
|
132
|
+
**Purpose**: Persist manual/non-tool findings as canonical event-backed observations.
|
|
133
|
+
**When to use**: Any time you manually confirm a finding that did not come from \`argus_slither_analyze\` or \`argus_check_patterns\` payloads.
|
|
134
|
+
**Arguments**:
|
|
135
|
+
- \`finding\` (string): Serialized JSON object for a single finding.
|
|
136
|
+
- \`findings\` (string): Serialized JSON array for multiple findings.
|
|
137
|
+
**Interpretation**:
|
|
138
|
+
- Recording is mandatory before handing findings to Argus for final synthesis.
|
|
139
|
+
|
|
130
140
|
## SKILL SYSTEM
|
|
131
141
|
|
|
132
142
|
Use \`argus_skill_load\` only when specialized context is needed before deep verification work.
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const initCommand: CliCommand = {
|
|
|
20
20
|
description: "Initialize Argus configuration for this project",
|
|
21
21
|
async execute(_args: string[]): Promise<number> {
|
|
22
22
|
const cwd = process.cwd()
|
|
23
|
-
const configDir = join(cwd, ".
|
|
23
|
+
const configDir = join(cwd, ".argus")
|
|
24
24
|
const configPath = join(configDir, "solidity-argus.json")
|
|
25
25
|
|
|
26
26
|
if (existsSync(configPath)) {
|
package/src/config/schema.ts
CHANGED
|
@@ -31,7 +31,7 @@ const ReportingConfigSchema = z.object({
|
|
|
31
31
|
format: z.enum(["markdown"]).default("markdown"),
|
|
32
32
|
severityThreshold: z.enum(["critical", "high", "medium", "low", "informational"]).default("low"),
|
|
33
33
|
gasAnalysis: z.boolean().default(false),
|
|
34
|
-
output_dir: z.string().default(".
|
|
34
|
+
output_dir: z.string().default(".argus/reports/"),
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
const SoloditConfigSchema = z.object({
|
|
@@ -74,7 +74,7 @@ export const ArgusConfigSchema = z.object({
|
|
|
74
74
|
format: "markdown",
|
|
75
75
|
severityThreshold: "low",
|
|
76
76
|
gasAnalysis: false,
|
|
77
|
-
output_dir: ".
|
|
77
|
+
output_dir: ".argus/reports/",
|
|
78
78
|
}),
|
|
79
79
|
solodit: SoloditConfigSchema.default({
|
|
80
80
|
enabled: true,
|
package/src/create-hooks.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { join } from "node:path"
|
|
2
1
|
import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
|
|
3
2
|
import type { ArgusConfig } from "./config/types"
|
|
4
3
|
import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
|
|
@@ -7,9 +6,12 @@ import {
|
|
|
7
6
|
createSessionRecoveryHandler,
|
|
8
7
|
createToolErrorRecoveryHandler,
|
|
9
8
|
} from "./features/error-recovery"
|
|
10
|
-
import { createDebouncedSave } from "./features/persistent-state/audit-state-manager"
|
|
11
9
|
import { getMigrationMode } from "./features/migration"
|
|
10
|
+
import { adaptLegacyFindings } from "./features/migration/migration-adapter"
|
|
11
|
+
import { computeParityMetrics, formatParityReport } from "./features/migration/parity-telemetry"
|
|
12
|
+
import { createDebouncedSave } from "./features/persistent-state/audit-state-manager"
|
|
12
13
|
import { createEventSink, type EventSink } from "./features/persistent-state/event-sink"
|
|
14
|
+
import { materializeFindings } from "./features/persistent-state/findings-materializer"
|
|
13
15
|
import { recordRun } from "./features/persistent-state/global-run-index"
|
|
14
16
|
import { createRunJournal } from "./features/persistent-state/run-journal"
|
|
15
17
|
import { createAgentTracker } from "./hooks/agent-tracker"
|
|
@@ -24,8 +26,8 @@ import { createSystemPromptHook } from "./hooks/system-prompt-hook"
|
|
|
24
26
|
import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
|
|
25
27
|
import type { HookName } from "./hooks/types"
|
|
26
28
|
import type { Managers } from "./managers/types"
|
|
27
|
-
import { createLogger } from "./shared/logger"
|
|
28
29
|
import { createAuditArtifactResolver } from "./shared/audit-artifact-resolver"
|
|
30
|
+
import { createLogger } from "./shared/logger"
|
|
29
31
|
import type { AuditState } from "./state/types"
|
|
30
32
|
import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
|
|
31
33
|
import { detectProject, type ProjectConfig } from "./utils/project-detector"
|
|
@@ -135,10 +137,8 @@ export function createHooks(args: {
|
|
|
135
137
|
|
|
136
138
|
const effectiveState = recoveredState ?? auditStateManager.get()
|
|
137
139
|
if (effectiveState) {
|
|
140
|
+
const resolver = createAuditArtifactResolver(effectiveState.sessionId, projectDir)
|
|
138
141
|
try {
|
|
139
|
-
// createAuditArtifactResolver is the canonical source for all run artifact paths.
|
|
140
|
-
// The journal file path is: {projectDir}/.opencode/runs/{runId}/events.jsonl
|
|
141
|
-
const resolver = createAuditArtifactResolver(effectiveState.sessionId, projectDir)
|
|
142
142
|
const journalFile = resolver.paths().journalFile
|
|
143
143
|
// createEventSink builds the same path internally; the resolver makes it explicit.
|
|
144
144
|
currentEventSink = createEventSink(effectiveState.sessionId, projectDir)
|
|
@@ -149,13 +149,12 @@ export function createHooks(args: {
|
|
|
149
149
|
`Failed to create event sink: ${error instanceof Error ? error.message : String(error)}`,
|
|
150
150
|
)
|
|
151
151
|
}
|
|
152
|
-
|
|
153
152
|
void recordRun({
|
|
154
153
|
runId: effectiveState.sessionId,
|
|
155
154
|
opencodeSessionId: sessionId,
|
|
156
155
|
projectDir: effectiveState.projectDir,
|
|
157
|
-
statePath:
|
|
158
|
-
journalPath:
|
|
156
|
+
statePath: resolver.paths().stateFile,
|
|
157
|
+
journalPath: resolver.paths().journalFile,
|
|
159
158
|
startedAt: effectiveState.startTime,
|
|
160
159
|
phase: effectiveState.currentPhase,
|
|
161
160
|
findingsCount: effectiveState.findings.length,
|
|
@@ -188,17 +187,36 @@ export function createHooks(args: {
|
|
|
188
187
|
toolsExecutedCount: auditState.toolsExecuted.length,
|
|
189
188
|
})
|
|
190
189
|
|
|
190
|
+
const idleResolver = createAuditArtifactResolver(
|
|
191
|
+
auditState.sessionId,
|
|
192
|
+
auditState.projectDir,
|
|
193
|
+
)
|
|
191
194
|
void recordRun({
|
|
192
195
|
runId: auditState.sessionId,
|
|
193
196
|
opencodeSessionId: sessionId,
|
|
194
197
|
projectDir: auditState.projectDir,
|
|
195
|
-
statePath:
|
|
196
|
-
journalPath:
|
|
198
|
+
statePath: idleResolver.paths().stateFile,
|
|
199
|
+
journalPath: idleResolver.paths().journalFile,
|
|
197
200
|
startedAt: auditState.startTime,
|
|
198
201
|
phase: auditState.currentPhase,
|
|
199
202
|
findingsCount: auditState.findings.length,
|
|
200
203
|
})
|
|
201
204
|
|
|
205
|
+
if (migrationMode !== "legacy") {
|
|
206
|
+
try {
|
|
207
|
+
const { legacyFindings, canonicalFindings } = adaptLegacyFindings(
|
|
208
|
+
auditState,
|
|
209
|
+
migrationMode,
|
|
210
|
+
auditState.sessionId,
|
|
211
|
+
)
|
|
212
|
+
const parityMetrics = computeParityMetrics(legacyFindings, canonicalFindings)
|
|
213
|
+
logger.debug(formatParityReport(parityMetrics))
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logger.warn(
|
|
216
|
+
`Migration parity check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
202
220
|
return
|
|
203
221
|
}
|
|
204
222
|
|
|
@@ -292,21 +310,82 @@ export function createHooks(args: {
|
|
|
292
310
|
{
|
|
293
311
|
getEventSink: () => currentEventSink,
|
|
294
312
|
getSessionId: () => currentOpencodeSessionId,
|
|
313
|
+
getAgentName: () => {
|
|
314
|
+
if (!currentOpencodeSessionId) {
|
|
315
|
+
return undefined
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const agent = agentTracker.getAgentForSession(currentOpencodeSessionId)
|
|
319
|
+
if (
|
|
320
|
+
agent === "argus" ||
|
|
321
|
+
agent === "sentinel" ||
|
|
322
|
+
agent === "pythia" ||
|
|
323
|
+
agent === "scribe" ||
|
|
324
|
+
agent === "unknown"
|
|
325
|
+
) {
|
|
326
|
+
return agent
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return "unknown"
|
|
330
|
+
},
|
|
295
331
|
},
|
|
296
332
|
),
|
|
297
333
|
"tool-tracking",
|
|
298
334
|
)
|
|
299
335
|
: undefined
|
|
300
336
|
|
|
337
|
+
const materializeCurrentFindings = async (
|
|
338
|
+
trigger: "session.idle" | "tool.execute.after",
|
|
339
|
+
failFast = false,
|
|
340
|
+
): Promise<void> => {
|
|
341
|
+
const state = getAuditState()
|
|
342
|
+
if (!state || state.sessionId.length === 0) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
await materializeFindings(
|
|
348
|
+
state.sessionId,
|
|
349
|
+
state.projectDir,
|
|
350
|
+
currentOpencodeSessionId.length > 0 ? currentOpencodeSessionId : undefined,
|
|
351
|
+
)
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (failFast) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`Failed to materialize findings artifact on ${trigger} for run ${state.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
logger.warn(
|
|
360
|
+
`Failed to materialize findings artifact on ${trigger} for run ${state.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
301
365
|
const safeEventHook = isHookEnabled("event")
|
|
302
366
|
? safeCreateHook(
|
|
303
367
|
() => async (input: Parameters<typeof eventHook>[0]) => {
|
|
304
368
|
const isSessionDeleted = input.event.type === "session.deleted"
|
|
369
|
+
const finalizationBeforeDelete = isSessionDeleted ? getLastFinalizationResult() : null
|
|
305
370
|
|
|
306
371
|
try {
|
|
307
372
|
await eventHook(input)
|
|
308
373
|
} finally {
|
|
309
374
|
if (isSessionDeleted) {
|
|
375
|
+
const finalizationResult = getLastFinalizationResult()
|
|
376
|
+
const hasNewFinalization =
|
|
377
|
+
finalizationResult !== null && finalizationResult !== finalizationBeforeDelete
|
|
378
|
+
|
|
379
|
+
if (hasNewFinalization && finalizationResult.runId.length > 0) {
|
|
380
|
+
try {
|
|
381
|
+
await materializeFindings(finalizationResult.runId, projectDir)
|
|
382
|
+
} catch (error) {
|
|
383
|
+
logger.warn(
|
|
384
|
+
`Failed to materialize findings artifact for run ${finalizationResult.runId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
310
389
|
await auditStateManager.archive()
|
|
311
390
|
|
|
312
391
|
const deletedSessionId = input.event.sessionId
|
|
@@ -318,7 +397,7 @@ export function createHooks(args: {
|
|
|
318
397
|
type: "session.deleted",
|
|
319
398
|
timestamp: Date.now(),
|
|
320
399
|
archived: true,
|
|
321
|
-
finalizationPassed:
|
|
400
|
+
finalizationPassed: finalizationResult?.invariantsPassed ?? null,
|
|
322
401
|
})
|
|
323
402
|
|
|
324
403
|
currentEventSink = null
|
|
@@ -360,6 +439,10 @@ export function createHooks(args: {
|
|
|
360
439
|
result: output.output,
|
|
361
440
|
})
|
|
362
441
|
|
|
442
|
+
if (input.tool === "argus_generate_report") {
|
|
443
|
+
await materializeCurrentFindings("tool.execute.after", true)
|
|
444
|
+
}
|
|
445
|
+
|
|
363
446
|
const outputWithHint = recoveryHint ? `${output.output}${recoveryHint}` : output.output
|
|
364
447
|
output.output = outputTruncator(outputWithHint)
|
|
365
448
|
}
|
package/src/create-tools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { forgeTestTool } from "./tools/forge-test-tool"
|
|
|
8
8
|
import { gasAnalysisTool } from "./tools/gas-analysis-tool"
|
|
9
9
|
import { patternCheckerTool } from "./tools/pattern-checker-tool"
|
|
10
10
|
import { proxyDetectionTool } from "./tools/proxy-detection-tool"
|
|
11
|
+
import { recordFindingTool } from "./tools/record-finding-tool"
|
|
11
12
|
import { reportGeneratorTool } from "./tools/report-generator-tool"
|
|
12
13
|
import { slitherTool } from "./tools/slither-tool"
|
|
13
14
|
import { createSoloditSearchTool } from "./tools/solodit-search-tool"
|
|
@@ -24,6 +25,7 @@ export function createTools(config: ArgusConfig): Record<string, ToolDefinition>
|
|
|
24
25
|
argus_check_patterns: patternCheckerTool,
|
|
25
26
|
argus_proxy_detection: proxyDetectionTool,
|
|
26
27
|
argus_skill_load: argusSkillLoadTool,
|
|
28
|
+
argus_record_finding: recordFindingTool,
|
|
27
29
|
argus_generate_report: reportGeneratorTool,
|
|
28
30
|
argus_sync_knowledge: syncKnowledgeTool,
|
|
29
31
|
}
|
|
@@ -11,6 +11,23 @@ const PHASE_ORDER: AuditPhase[] = [
|
|
|
11
11
|
"complete",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
|
+
const REPORTING_PHASES: AuditPhase[] = ["reporting", "complete"]
|
|
15
|
+
|
|
16
|
+
const KEY_TOOL_FAMILIES: Array<{ family: string; prefixes: string[] }> = [
|
|
17
|
+
{ family: "slither", prefixes: ["argus_slither_analyze", "slither"] },
|
|
18
|
+
{ family: "forge_test", prefixes: ["argus_forge_test", "forge_test"] },
|
|
19
|
+
{ family: "forge_fuzz", prefixes: ["argus_forge_fuzz", "forge_fuzz"] },
|
|
20
|
+
{ family: "forge_coverage", prefixes: ["argus_forge_coverage", "forge_coverage"] },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
function getMissingToolFamilies(auditState: AuditState): string[] {
|
|
24
|
+
const executedTools = auditState.toolsExecuted.map((t) => t.tool)
|
|
25
|
+
return KEY_TOOL_FAMILIES.filter(
|
|
26
|
+
({ prefixes }) =>
|
|
27
|
+
!executedTools.some((tool) => prefixes.some((prefix) => tool.startsWith(prefix))),
|
|
28
|
+
).map(({ family }) => family)
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
function getNextPhase(current: AuditPhase): AuditPhase | null {
|
|
15
32
|
const idx = PHASE_ORDER.indexOf(current)
|
|
16
33
|
if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null
|
|
@@ -25,10 +42,21 @@ export function createAuditEnforcer() {
|
|
|
25
42
|
const nextPhase = getNextPhase(auditState.currentPhase)
|
|
26
43
|
if (!nextPhase) return null
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
const parts: string[] = [
|
|
29
46
|
`[Argus Audit Enforcer] Audit in progress — current phase: ${auditState.currentPhase}.`,
|
|
30
47
|
`Next phase: ${nextPhase}. Do not stop until audit is complete.`,
|
|
31
48
|
`Progress: ${auditState.findings.length} findings, ${auditState.contractsReviewed.length} contracts reviewed.`,
|
|
32
|
-
]
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
if (REPORTING_PHASES.includes(auditState.currentPhase)) {
|
|
52
|
+
const missing = getMissingToolFamilies(auditState)
|
|
53
|
+
if (missing.length > 0) {
|
|
54
|
+
parts.push(
|
|
55
|
+
`\u26a0\ufe0f Tool coverage incomplete: ${missing.join(", ")} have not been executed. Do not proceed to report generation until required tools are complete.`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parts.join(" ")
|
|
33
61
|
}
|
|
34
62
|
}
|