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.
- package/AGENTS.md +13 -6
- package/README.md +24 -12
- package/package.json +7 -3
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +1 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +1 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +1 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +1 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +1 -0
- package/skills/checklists/general-audit/SKILL.md +1 -0
- package/skills/methodology/audit-workflow/SKILL.md +1 -0
- package/skills/methodology/report-template/SKILL.md +1 -0
- package/skills/methodology/severity-classification/SKILL.md +1 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +1 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +1 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +1 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +1 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +1 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +0 -50
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +0 -63
- package/src/agents/argus-prompt.ts +98 -33
- package/src/agents/pythia-prompt.ts +18 -1
- package/src/agents/scribe-prompt.ts +32 -10
- package/src/agents/sentinel-prompt.ts +19 -0
- package/src/agents/themis-prompt.ts +110 -0
- package/src/cli/commands/doctor.ts +29 -17
- package/src/config/loader.ts +29 -5
- package/src/config/schema.ts +45 -45
- package/src/constants/defaults.ts +1 -0
- package/src/create-hooks.ts +797 -148
- package/src/create-managers.ts +4 -2
- package/src/create-tools.ts +5 -1
- package/src/features/audit-enforcer/audit-enforcer.ts +1 -11
- package/src/features/background-agent/background-manager.ts +32 -5
- package/src/features/error-recovery/tool-error-recovery.ts +1 -0
- package/src/features/persistent-state/audit-state-manager.ts +272 -29
- package/src/features/persistent-state/event-sink.ts +96 -25
- package/src/features/persistent-state/findings-materializer.ts +34 -2
- package/src/features/persistent-state/global-run-index.ts +86 -8
- package/src/features/persistent-state/index.ts +7 -1
- package/src/features/persistent-state/run-finalizer.ts +116 -7
- package/src/features/persistent-state/run-pruner.ts +93 -0
- package/src/hooks/agent-tracker.ts +14 -2
- package/src/hooks/compaction-hook.ts +7 -16
- package/src/hooks/config-handler.ts +83 -29
- package/src/hooks/context-budget.ts +4 -5
- package/src/hooks/event-hook.ts +213 -57
- package/src/hooks/knowledge-sync-hook.ts +2 -3
- package/src/hooks/safe-create-hook.ts +13 -1
- package/src/hooks/system-prompt-hook.ts +20 -39
- package/src/hooks/tool-tracking-hook.ts +597 -323
- package/src/index.ts +15 -1
- package/src/knowledge/scvd-client.ts +2 -4
- package/src/knowledge/scvd-errors.ts +25 -2
- package/src/knowledge/scvd-index.ts +7 -5
- package/src/knowledge/scvd-sync.ts +6 -6
- package/src/managers/types.ts +20 -2
- package/src/shared/agent-names.ts +23 -0
- package/src/shared/audit-artifact-resolver.ts +8 -3
- package/src/shared/audit-phases.ts +12 -0
- package/src/shared/cache-paths.ts +41 -0
- package/src/shared/drop-diagnostics.ts +2 -2
- package/src/shared/forge-errors.ts +31 -0
- package/src/shared/forge-runner.ts +30 -0
- package/src/shared/format-error.ts +3 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/key-tools.ts +39 -0
- package/src/shared/logger.ts +7 -7
- package/src/shared/path-containment.ts +25 -0
- package/src/shared/path-utils.ts +11 -0
- package/src/shared/report-path-resolver.ts +4 -2
- package/src/shared/safe-emit.ts +24 -0
- package/src/shared/token-utils.ts +5 -0
- package/src/shared/type-guards.ts +8 -0
- package/src/shared/validation-constants.ts +52 -0
- package/src/skills/analysis/cluster.ts +1 -114
- package/src/skills/analysis/normalize.ts +2 -114
- package/src/skills/analysis/stopwords.ts +109 -0
- package/src/skills/argus-skill-resolver.ts +6 -3
- package/src/solodit-lifecycle.ts +153 -37
- package/src/state/adapters.ts +60 -66
- package/src/state/finding-aggregation.ts +6 -8
- package/src/state/finding-fingerprint.ts +1 -1
- package/src/state/finding-store.ts +31 -9
- package/src/state/index.ts +1 -1
- package/src/state/projectors.ts +27 -19
- package/src/state/schemas.ts +8 -32
- package/src/state/types.ts +3 -0
- package/src/tools/contract-analyzer-tool.ts +4 -6
- package/src/tools/forge-coverage-tool.ts +10 -35
- package/src/tools/forge-fuzz-tool.ts +21 -51
- package/src/tools/forge-test-tool.ts +25 -47
- package/src/tools/gas-analysis-tool.ts +12 -41
- package/src/tools/pattern-checker-tool.ts +37 -15
- package/src/tools/pattern-loader.ts +18 -4
- package/src/tools/persist-deduped-tool.ts +94 -0
- package/src/tools/proxy-detection-tool.ts +35 -34
- package/src/tools/read-findings-tool.ts +390 -0
- package/src/tools/record-finding-tool.ts +120 -25
- package/src/tools/report-generator-tool.ts +394 -328
- package/src/tools/report-preflight.ts +5 -1
- package/src/tools/slither-tool.ts +55 -16
- package/src/tools/solodit-search-tool.ts +260 -112
- package/src/tools/sync-knowledge-tool.ts +2 -3
- package/src/utils/solidity-parser.ts +39 -24
- package/src/features/migration/index.ts +0 -14
- package/src/features/migration/migration-adapter.ts +0 -151
- package/src/features/migration/parity-telemetry.ts +0 -133
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto"
|
|
2
2
|
import type { EventSink } from "../features/persistent-state/event-sink"
|
|
3
|
+
import { isArgusFamily } from "../shared/agent-names"
|
|
4
|
+
import { PHASE_ORDER } from "../shared/audit-phases"
|
|
3
5
|
import type {
|
|
4
6
|
DropDiagnostic,
|
|
5
7
|
DropDiagnosticsCollector,
|
|
@@ -7,6 +9,8 @@ import type {
|
|
|
7
9
|
} from "../shared/drop-diagnostics"
|
|
8
10
|
import { createDropDiagnosticsCollector } from "../shared/drop-diagnostics"
|
|
9
11
|
import { createLogger } from "../shared/logger"
|
|
12
|
+
import { normalizeFilePath } from "../shared/path-utils"
|
|
13
|
+
import { safeEmitToSink } from "../shared/safe-emit"
|
|
10
14
|
import { normalizeToCanonicalFinding } from "../state/adapters"
|
|
11
15
|
import type { FindingStore } from "../state/finding-store"
|
|
12
16
|
import { createFindingStore } from "../state/finding-store"
|
|
@@ -34,15 +38,20 @@ type ToolHookInput = {
|
|
|
34
38
|
type ToolExecutionMetadata = {
|
|
35
39
|
tool: string
|
|
36
40
|
findingsCount: number
|
|
41
|
+
sessionId?: string
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export type ToolTrackingOptions = {
|
|
40
45
|
getEventSink?: () => EventSink | null
|
|
46
|
+
getEventSinkForSession?: (sessionId: string) => EventSink | null
|
|
47
|
+
getEventSinkForRun?: (runId: string) => EventSink | null
|
|
48
|
+
getActiveRunSinks?: () => EventSink[]
|
|
41
49
|
getSessionId?: () => string
|
|
42
50
|
getAgentName?: () => ArgusAgentName | undefined
|
|
43
51
|
getAgentNameForSession?: (sessionId: string) => ArgusAgentName | undefined
|
|
44
52
|
dropPolicy?: DropPolicy
|
|
45
53
|
onChildSessionDetected?: (parentSessionId: string, childSessionId: string) => void
|
|
54
|
+
projectDir?: string
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
const VALID_SEVERITIES: ReadonlySet<string> = new Set([
|
|
@@ -103,22 +112,7 @@ function toFindingSource(value: unknown): Finding["source"] {
|
|
|
103
112
|
return "manual"
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
sink: EventSink,
|
|
108
|
-
event: AuditEvent,
|
|
109
|
-
options?: { failFast?: boolean },
|
|
110
|
-
): Promise<void> {
|
|
111
|
-
try {
|
|
112
|
-
await sink.append(event)
|
|
113
|
-
} catch (error) {
|
|
114
|
-
const message = `Failed to emit ${event.type} event to sink: ${error instanceof Error ? error.message : String(error)}`
|
|
115
|
-
logger.error(message)
|
|
116
|
-
|
|
117
|
-
if (options?.failFast) {
|
|
118
|
-
throw new Error(message)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
115
|
+
const emitToSink = safeEmitToSink
|
|
122
116
|
|
|
123
117
|
function buildEvent(
|
|
124
118
|
type: AuditEvent["type"],
|
|
@@ -146,6 +140,7 @@ function buildEvent(
|
|
|
146
140
|
* or plain text with an embedded JSON fragment.
|
|
147
141
|
*/
|
|
148
142
|
function parseChildSessionId(result: string): string | null {
|
|
143
|
+
// Strategy 1: Full JSON parse (structured tool output)
|
|
149
144
|
try {
|
|
150
145
|
const parsed = JSON.parse(result)
|
|
151
146
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
@@ -163,11 +158,32 @@ function parseChildSessionId(result: string): string | null {
|
|
|
163
158
|
}
|
|
164
159
|
}
|
|
165
160
|
} catch {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
// Not valid JSON — fall through to regex strategies
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Strategy 2: OpenCode task tool XML format
|
|
165
|
+
// <task_metadata>
|
|
166
|
+
// session_id: ses_xxx
|
|
167
|
+
// </task_metadata>
|
|
168
|
+
const xmlMatch = result.match(
|
|
169
|
+
/<task_metadata>[\s\S]*?session_id:\s*(ses_\S+)[\s\S]*?<\/task_metadata>/,
|
|
170
|
+
)
|
|
171
|
+
if (xmlMatch?.[1]) {
|
|
172
|
+
return xmlMatch[1]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Strategy 3: JSON fragment in plain text
|
|
176
|
+
const jsonFragmentMatch = result.match(/"session_id"\s*:\s*"([^"]+)"/)
|
|
177
|
+
if (jsonFragmentMatch?.[1]) {
|
|
178
|
+
return jsonFragmentMatch[1]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Strategy 4: Bare session_id line (e.g. "session_id: ses_xxx" outside XML tags)
|
|
182
|
+
const bareMatch = result.match(/session_id:\s*(ses_\S+)/)
|
|
183
|
+
if (bareMatch?.[1]) {
|
|
184
|
+
return bareMatch[1]
|
|
170
185
|
}
|
|
186
|
+
|
|
171
187
|
return null
|
|
172
188
|
}
|
|
173
189
|
|
|
@@ -190,141 +206,100 @@ const SLITHER_REQUIRED = ["check", "description", "file", "lines"] as const
|
|
|
190
206
|
const PATTERN_REQUIRED = ["pattern", "description", "file", "lines"] as const
|
|
191
207
|
const MANUAL_REQUIRED = ["check", "description", "file", "lines"] as const
|
|
192
208
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!finding) continue
|
|
206
|
-
|
|
207
|
-
const check = finding.check
|
|
208
|
-
const description = finding.description
|
|
209
|
-
const file = finding.file
|
|
210
|
-
const lines = toLines(finding.lines)
|
|
209
|
+
type ProcessorConfig = {
|
|
210
|
+
toolLabel: string
|
|
211
|
+
arrayKey: string
|
|
212
|
+
nestedArrayKey?: string
|
|
213
|
+
primaryIdField: string
|
|
214
|
+
requiredFields: readonly string[]
|
|
215
|
+
sourceLabel: string | "dynamic"
|
|
216
|
+
confidenceMode: "read" | "fixed"
|
|
217
|
+
confidenceDefault?: string
|
|
218
|
+
extractOptionalFields: boolean
|
|
219
|
+
allowReportedByOverride: boolean
|
|
220
|
+
}
|
|
211
221
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
missing[0],
|
|
223
|
-
)
|
|
224
|
-
continue
|
|
225
|
-
}
|
|
222
|
+
const SLITHER_CONFIG: ProcessorConfig = {
|
|
223
|
+
toolLabel: "Slither",
|
|
224
|
+
arrayKey: "findings",
|
|
225
|
+
primaryIdField: "check",
|
|
226
|
+
requiredFields: SLITHER_REQUIRED,
|
|
227
|
+
sourceLabel: "slither",
|
|
228
|
+
confidenceMode: "read",
|
|
229
|
+
extractOptionalFields: false,
|
|
230
|
+
allowReportedByOverride: false,
|
|
231
|
+
}
|
|
226
232
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
233
|
+
const PATTERN_CONFIG: ProcessorConfig = {
|
|
234
|
+
toolLabel: "Pattern",
|
|
235
|
+
arrayKey: "sources",
|
|
236
|
+
nestedArrayKey: "matches",
|
|
237
|
+
primaryIdField: "pattern",
|
|
238
|
+
requiredFields: PATTERN_REQUIRED,
|
|
239
|
+
sourceLabel: "pattern",
|
|
240
|
+
confidenceMode: "fixed",
|
|
241
|
+
confidenceDefault: "Medium",
|
|
242
|
+
extractOptionalFields: false,
|
|
243
|
+
allowReportedByOverride: false,
|
|
244
|
+
}
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
const RECORDED_CONFIG: ProcessorConfig = {
|
|
247
|
+
toolLabel: "Recorded",
|
|
248
|
+
arrayKey: "findings",
|
|
249
|
+
primaryIdField: "check",
|
|
250
|
+
requiredFields: MANUAL_REQUIRED,
|
|
251
|
+
sourceLabel: "dynamic",
|
|
252
|
+
confidenceMode: "read",
|
|
253
|
+
extractOptionalFields: true,
|
|
254
|
+
allowReportedByOverride: true,
|
|
242
255
|
}
|
|
243
256
|
|
|
244
|
-
function
|
|
257
|
+
function processToolResult(
|
|
245
258
|
parsed: Record<string, unknown>,
|
|
246
259
|
store: FindingStore,
|
|
247
260
|
diag: DropDiagnosticsCollector,
|
|
248
261
|
metadata: { reportedByAgent: ArgusAgentName; reportedBySessionId: string },
|
|
262
|
+
config: ProcessorConfig,
|
|
263
|
+
projectDir?: string,
|
|
249
264
|
): number {
|
|
250
|
-
const
|
|
251
|
-
if (!Array.isArray(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const matches = source.matches
|
|
259
|
-
if (!Array.isArray(matches)) continue
|
|
260
|
-
|
|
261
|
-
for (const rawMatch of matches) {
|
|
262
|
-
const match = toRecord(rawMatch)
|
|
263
|
-
if (!match) continue
|
|
264
|
-
|
|
265
|
-
const pattern = match.pattern
|
|
266
|
-
const description = match.description
|
|
267
|
-
const file = match.file
|
|
268
|
-
const lines = toLines(match.lines)
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
typeof pattern !== "string" ||
|
|
272
|
-
typeof description !== "string" ||
|
|
273
|
-
typeof file !== "string" ||
|
|
274
|
-
!lines
|
|
275
|
-
) {
|
|
276
|
-
const missing = identifyMissingFields(match, PATTERN_REQUIRED)
|
|
277
|
-
diag.error(
|
|
278
|
-
"MISSING_REQUIRED_FIELD",
|
|
279
|
-
`Pattern finding skipped: missing ${missing.join(", ")}`,
|
|
280
|
-
missing[0],
|
|
281
|
-
)
|
|
282
|
-
continue
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
store.addFinding({
|
|
286
|
-
check: pattern,
|
|
287
|
-
severity: toSeverity(match.severity),
|
|
288
|
-
confidence: "Medium",
|
|
289
|
-
description,
|
|
290
|
-
file,
|
|
291
|
-
lines,
|
|
292
|
-
source: "pattern",
|
|
293
|
-
reported_by_agent: metadata.reportedByAgent,
|
|
294
|
-
reported_by_session_id: metadata.reportedBySessionId,
|
|
295
|
-
})
|
|
296
|
-
count++
|
|
265
|
+
const topLevel = parsed[config.arrayKey]
|
|
266
|
+
if (!Array.isArray(topLevel)) {
|
|
267
|
+
if (config.toolLabel === "Recorded") {
|
|
268
|
+
diag.error(
|
|
269
|
+
"MISSING_REQUIRED_FIELD",
|
|
270
|
+
"argus_record_finding result missing findings array",
|
|
271
|
+
"findings",
|
|
272
|
+
)
|
|
297
273
|
}
|
|
274
|
+
return 0
|
|
298
275
|
}
|
|
299
276
|
|
|
300
|
-
|
|
301
|
-
|
|
277
|
+
const items: unknown[] = []
|
|
278
|
+
if (config.nestedArrayKey) {
|
|
279
|
+
for (const rawOuter of topLevel) {
|
|
280
|
+
const outer = toRecord(rawOuter)
|
|
281
|
+
if (!outer) continue
|
|
302
282
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (!Array.isArray(findings)) {
|
|
311
|
-
diag.error(
|
|
312
|
-
"MISSING_REQUIRED_FIELD",
|
|
313
|
-
"argus_record_finding result missing findings array",
|
|
314
|
-
"findings",
|
|
315
|
-
)
|
|
316
|
-
return 0
|
|
283
|
+
const nested = outer[config.nestedArrayKey]
|
|
284
|
+
if (!Array.isArray(nested)) continue
|
|
285
|
+
|
|
286
|
+
items.push(...nested)
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
items.push(...topLevel)
|
|
317
290
|
}
|
|
318
291
|
|
|
319
292
|
let count = 0
|
|
320
|
-
for (const
|
|
321
|
-
const
|
|
322
|
-
if (!
|
|
293
|
+
for (const rawItem of items) {
|
|
294
|
+
const item = toRecord(rawItem)
|
|
295
|
+
if (!item) continue
|
|
323
296
|
|
|
324
|
-
const check =
|
|
325
|
-
const description =
|
|
326
|
-
const
|
|
327
|
-
const
|
|
297
|
+
const check = item[config.primaryIdField]
|
|
298
|
+
const description = item.description
|
|
299
|
+
const rawFile = item.file
|
|
300
|
+
const file =
|
|
301
|
+
typeof rawFile === "string" && projectDir ? normalizeFilePath(rawFile, projectDir) : rawFile
|
|
302
|
+
const lines = toLines(item.lines)
|
|
328
303
|
|
|
329
304
|
if (
|
|
330
305
|
typeof check !== "string" ||
|
|
@@ -332,51 +307,60 @@ function processRecordedFindingResult(
|
|
|
332
307
|
typeof file !== "string" ||
|
|
333
308
|
!lines
|
|
334
309
|
) {
|
|
335
|
-
const missing = identifyMissingFields(
|
|
310
|
+
const missing = identifyMissingFields(item, config.requiredFields)
|
|
336
311
|
diag.error(
|
|
337
312
|
"MISSING_REQUIRED_FIELD",
|
|
338
|
-
|
|
313
|
+
`${config.toolLabel} finding skipped: missing ${missing.join(", ")}`,
|
|
339
314
|
missing[0],
|
|
340
315
|
)
|
|
341
316
|
continue
|
|
342
317
|
}
|
|
343
318
|
|
|
344
|
-
const reportedByAgentRaw =
|
|
319
|
+
const reportedByAgentRaw = item.reported_by_agent
|
|
345
320
|
const reportedByAgent =
|
|
346
|
-
|
|
347
|
-
reportedByAgentRaw === "
|
|
348
|
-
reportedByAgentRaw === "
|
|
349
|
-
reportedByAgentRaw === "scribe" ||
|
|
350
|
-
reportedByAgentRaw === "unknown"
|
|
321
|
+
config.allowReportedByOverride &&
|
|
322
|
+
typeof reportedByAgentRaw === "string" &&
|
|
323
|
+
(isArgusFamily(reportedByAgentRaw) || reportedByAgentRaw === "unknown")
|
|
351
324
|
? (reportedByAgentRaw as ArgusAgentName)
|
|
352
325
|
: metadata.reportedByAgent
|
|
353
326
|
|
|
354
|
-
|
|
327
|
+
const findingPayload: Parameters<FindingStore["addFinding"]>[0] = {
|
|
355
328
|
check,
|
|
356
|
-
severity: toSeverity(
|
|
357
|
-
confidence:
|
|
329
|
+
severity: toSeverity(item.severity),
|
|
330
|
+
confidence:
|
|
331
|
+
config.confidenceMode === "read"
|
|
332
|
+
? toConfidence(item.confidence)
|
|
333
|
+
: toConfidence(config.confidenceDefault),
|
|
358
334
|
description,
|
|
359
335
|
file,
|
|
360
336
|
lines,
|
|
361
|
-
source:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
337
|
+
source:
|
|
338
|
+
config.sourceLabel === "dynamic"
|
|
339
|
+
? toFindingSource(item.source)
|
|
340
|
+
: toFindingSource(config.sourceLabel),
|
|
365
341
|
reported_by_agent: reportedByAgent,
|
|
366
342
|
reported_by_session_id:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
343
|
+
config.allowReportedByOverride &&
|
|
344
|
+
typeof item.reported_by_session_id === "string" &&
|
|
345
|
+
item.reported_by_session_id.length > 0
|
|
346
|
+
? item.reported_by_session_id
|
|
370
347
|
: metadata.reportedBySessionId,
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (config.extractOptionalFields) {
|
|
351
|
+
findingPayload.remediation =
|
|
352
|
+
typeof item.remediation === "string" ? item.remediation : undefined
|
|
353
|
+
findingPayload.exploitReference =
|
|
354
|
+
typeof item.exploitReference === "string" ? item.exploitReference : undefined
|
|
355
|
+
findingPayload.issue_fingerprint =
|
|
356
|
+
typeof item.issue_fingerprint === "string" ? item.issue_fingerprint : undefined
|
|
357
|
+
findingPayload.observation_fingerprint =
|
|
358
|
+
typeof item.observation_fingerprint === "string" ? item.observation_fingerprint : undefined
|
|
359
|
+
findingPayload.observation_id =
|
|
360
|
+
typeof item.observation_id === "string" ? item.observation_id : undefined
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
store.addFinding(findingPayload)
|
|
380
364
|
count++
|
|
381
365
|
}
|
|
382
366
|
|
|
@@ -472,21 +456,77 @@ function recordToolExecution(state: AuditState, toolName: string, findingsCount:
|
|
|
472
456
|
})
|
|
473
457
|
}
|
|
474
458
|
|
|
459
|
+
const TOOL_PHASE_MAP: Record<string, AuditState["currentPhase"]> = {
|
|
460
|
+
argus_slither_analyze: "scanning",
|
|
461
|
+
argus_check_patterns: "scanning",
|
|
462
|
+
argus_analyze_contract: "scanning",
|
|
463
|
+
argus_proxy_detection: "scanning",
|
|
464
|
+
argus_solodit_search: "research",
|
|
465
|
+
argus_forge_test: "testing",
|
|
466
|
+
argus_forge_fuzz: "testing",
|
|
467
|
+
argus_forge_coverage: "testing",
|
|
468
|
+
argus_gas_analysis: "testing",
|
|
469
|
+
argus_generate_report: "reporting",
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function inferPhaseAdvancement(
|
|
473
|
+
state: AuditState,
|
|
474
|
+
toolName: string,
|
|
475
|
+
): AuditState["currentPhase"] | null {
|
|
476
|
+
const inferredPhase = TOOL_PHASE_MAP[toolName]
|
|
477
|
+
if (!inferredPhase) return null
|
|
478
|
+
|
|
479
|
+
const currentIdx = PHASE_ORDER.indexOf(state.currentPhase)
|
|
480
|
+
const inferredIdx = PHASE_ORDER.indexOf(inferredPhase)
|
|
481
|
+
if (inferredIdx <= currentIdx) return null
|
|
482
|
+
|
|
483
|
+
return inferredPhase
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
type OrphanEvent = {
|
|
487
|
+
event: AuditEvent
|
|
488
|
+
failFast: boolean
|
|
489
|
+
bufferedAt: number
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const ORPHAN_BUFFER_TTL_MS = 60_000
|
|
493
|
+
const MAX_ORPHAN_EVENTS_PER_SESSION = 50
|
|
494
|
+
|
|
475
495
|
export type ToolTrackingHook = {
|
|
476
496
|
(input: ToolHookInput): Promise<void>
|
|
477
497
|
getLastDiagnostics(): DropDiagnostic[]
|
|
498
|
+
flushOrphanEvents(sessionId: string, sink: EventSink): Promise<number>
|
|
478
499
|
}
|
|
479
500
|
|
|
480
501
|
export function createToolTrackingHook(
|
|
481
|
-
getAuditState: () => AuditState | null,
|
|
502
|
+
getAuditState: (sessionId?: string) => AuditState | null,
|
|
482
503
|
onStateChanged?: (metadata: ToolExecutionMetadata) => void,
|
|
483
504
|
options?: ToolTrackingOptions,
|
|
484
505
|
): ToolTrackingHook {
|
|
506
|
+
const projectDir = options?.projectDir
|
|
485
507
|
const storesByState = new WeakMap<AuditState, FindingStore>()
|
|
486
508
|
let lastDiagnostics: DropDiagnostic[] = []
|
|
509
|
+
const orphanBuffer = new Map<string, OrphanEvent[]>()
|
|
487
510
|
|
|
488
|
-
function
|
|
489
|
-
|
|
511
|
+
function bufferOrphanEvent(sessionId: string, entry: OrphanEvent): void {
|
|
512
|
+
let entries = orphanBuffer.get(sessionId)
|
|
513
|
+
if (!entries) {
|
|
514
|
+
entries = []
|
|
515
|
+
orphanBuffer.set(sessionId, entries)
|
|
516
|
+
}
|
|
517
|
+
if (entries.length >= MAX_ORPHAN_EVENTS_PER_SESSION) {
|
|
518
|
+
logger.warn(
|
|
519
|
+
`Orphan event buffer full for session ${sessionId} (${MAX_ORPHAN_EVENTS_PER_SESSION} events) — dropping oldest`,
|
|
520
|
+
)
|
|
521
|
+
entries.shift()
|
|
522
|
+
}
|
|
523
|
+
entries.push(entry)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function resolveStateAndStore(
|
|
527
|
+
sessionId?: string,
|
|
528
|
+
): { state: AuditState; store: FindingStore } | null {
|
|
529
|
+
const state = getAuditState(sessionId)
|
|
490
530
|
if (!state) return null
|
|
491
531
|
|
|
492
532
|
let store = storesByState.get(state)
|
|
@@ -503,8 +543,7 @@ export function createToolTrackingHook(
|
|
|
503
543
|
if (input.tool === "task") {
|
|
504
544
|
const childSessionId = parseChildSessionId(input.result)
|
|
505
545
|
const correlationId = randomUUID()
|
|
506
|
-
const resolved = resolveStateAndStore()
|
|
507
|
-
const sink = options?.getEventSink?.()
|
|
546
|
+
const resolved = resolveStateAndStore(input.sessionID)
|
|
508
547
|
const sessionId = input.sessionID ?? options?.getSessionId?.() ?? ""
|
|
509
548
|
const toolCallId = randomUUID()
|
|
510
549
|
|
|
@@ -512,6 +551,26 @@ export function createToolTrackingHook(
|
|
|
512
551
|
options?.onChildSessionDetected?.(sessionId, childSessionId)
|
|
513
552
|
}
|
|
514
553
|
|
|
554
|
+
let sink: EventSink | null =
|
|
555
|
+
(sessionId ? options?.getEventSinkForSession?.(sessionId) : null) ??
|
|
556
|
+
options?.getEventSink?.() ??
|
|
557
|
+
null
|
|
558
|
+
|
|
559
|
+
if (sink && resolved) {
|
|
560
|
+
const runId = resolved.state.sessionId
|
|
561
|
+
if (sink.runId !== runId) {
|
|
562
|
+
const runScopedSink = options?.getEventSinkForRun?.(runId) ?? null
|
|
563
|
+
if (runScopedSink && runScopedSink.runId === runId) {
|
|
564
|
+
sink = runScopedSink
|
|
565
|
+
} else {
|
|
566
|
+
logger.warn(
|
|
567
|
+
`Skipping task sink emission due to run mismatch: state run ${runId}, sink run ${sink.runId}`,
|
|
568
|
+
)
|
|
569
|
+
sink = null
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
515
574
|
if (sink && resolved) {
|
|
516
575
|
const runId = resolved.state.sessionId
|
|
517
576
|
await emitToSink(
|
|
@@ -538,7 +597,7 @@ export function createToolTrackingHook(
|
|
|
538
597
|
|
|
539
598
|
if (resolved) {
|
|
540
599
|
recordToolExecution(resolved.state, "task", 0)
|
|
541
|
-
onStateChanged?.({ tool: "task", findingsCount: 0 })
|
|
600
|
+
onStateChanged?.({ tool: "task", findingsCount: 0, sessionId: input.sessionID })
|
|
542
601
|
}
|
|
543
602
|
|
|
544
603
|
return
|
|
@@ -548,7 +607,7 @@ export function createToolTrackingHook(
|
|
|
548
607
|
return
|
|
549
608
|
}
|
|
550
609
|
|
|
551
|
-
const resolved = resolveStateAndStore()
|
|
610
|
+
const resolved = resolveStateAndStore(input.sessionID)
|
|
552
611
|
if (!resolved) {
|
|
553
612
|
if (input.tool === "argus_record_finding") {
|
|
554
613
|
throw new Error("argus_record_finding requires active audit state")
|
|
@@ -577,9 +636,34 @@ export function createToolTrackingHook(
|
|
|
577
636
|
}
|
|
578
637
|
|
|
579
638
|
const { state: auditState, store } = resolved
|
|
580
|
-
const sink = options?.getEventSink?.()
|
|
581
639
|
const runId = auditState.sessionId
|
|
582
640
|
const sessionId = input.sessionID ?? options?.getSessionId?.() ?? ""
|
|
641
|
+
let sink: EventSink | null =
|
|
642
|
+
(sessionId ? options?.getEventSinkForSession?.(sessionId) : null) ??
|
|
643
|
+
options?.getEventSink?.() ??
|
|
644
|
+
null
|
|
645
|
+
if (sink && sink.runId !== runId) {
|
|
646
|
+
const runScopedSink = options?.getEventSinkForRun?.(runId) ?? null
|
|
647
|
+
if (runScopedSink && runScopedSink.runId === runId) {
|
|
648
|
+
sink = runScopedSink
|
|
649
|
+
} else {
|
|
650
|
+
// Single-run coalescence: if exactly one active (non-finalized) sink
|
|
651
|
+
// exists, use it rather than dropping events silently.
|
|
652
|
+
const activeSinks = options?.getActiveRunSinks?.() ?? []
|
|
653
|
+
const coalescedSink = activeSinks.length === 1 ? activeSinks[0] : undefined
|
|
654
|
+
if (coalescedSink) {
|
|
655
|
+
logger.debug(
|
|
656
|
+
`Coalescing tool ${input.tool} from session ${sessionId} into active run ${coalescedSink.runId} (state run ${runId}, original sink run ${sink.runId})`,
|
|
657
|
+
)
|
|
658
|
+
sink = coalescedSink
|
|
659
|
+
} else {
|
|
660
|
+
logger.warn(
|
|
661
|
+
`Skipping sink emission for ${input.tool} due to run mismatch: state run ${runId}, sink run ${sink.runId}`,
|
|
662
|
+
)
|
|
663
|
+
sink = null
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
583
667
|
const reportedByAgent =
|
|
584
668
|
(input.sessionID ? options?.getAgentNameForSession?.(input.sessionID) : undefined) ??
|
|
585
669
|
options?.getAgentName?.() ??
|
|
@@ -601,186 +685,376 @@ export function createToolTrackingHook(
|
|
|
601
685
|
}),
|
|
602
686
|
{ failFast: input.tool === "argus_record_finding" },
|
|
603
687
|
)
|
|
688
|
+
} else if (sessionId) {
|
|
689
|
+
const event = buildEvent("tool.started", runId, sessionId, toolCallId, {
|
|
690
|
+
tool: input.tool,
|
|
691
|
+
args: input.args,
|
|
692
|
+
})
|
|
693
|
+
bufferOrphanEvent(sessionId, {
|
|
694
|
+
event,
|
|
695
|
+
failFast: input.tool === "argus_record_finding",
|
|
696
|
+
bufferedAt: Date.now(),
|
|
697
|
+
})
|
|
698
|
+
logger.warn(
|
|
699
|
+
`Buffered orphan tool.started event for ${input.tool} from session ${sessionId} (run_id=${runId})`,
|
|
700
|
+
)
|
|
604
701
|
}
|
|
605
702
|
|
|
606
703
|
const findingsCountBefore = auditState.findings.length
|
|
704
|
+
let findingsCount = 0
|
|
705
|
+
let completedSuccess = false
|
|
706
|
+
let completionError: string | undefined
|
|
607
707
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
auditState.skillsLoaded.
|
|
708
|
+
try {
|
|
709
|
+
if (input.tool === "argus_skill_load") {
|
|
710
|
+
const nameMatch = input.result.match(/^##\s+Argus Skill:\s+(.+?)(?:\s+\[|$)/m)
|
|
711
|
+
const skillName = nameMatch?.[1]?.trim()
|
|
712
|
+
if (skillName) {
|
|
713
|
+
auditState.skillsLoaded ??= []
|
|
714
|
+
if (!auditState.skillsLoaded.includes(skillName)) {
|
|
715
|
+
auditState.skillsLoaded.push(skillName)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
findingsCount = 0
|
|
719
|
+
completedSuccess = true
|
|
720
|
+
} else {
|
|
721
|
+
let parsed: unknown
|
|
722
|
+
try {
|
|
723
|
+
parsed = JSON.parse(input.result)
|
|
724
|
+
} catch {
|
|
725
|
+
// For large tool outputs (e.g. argus_check_patterns can produce 3MB+),
|
|
726
|
+
// OpenCode may truncate the result before it reaches this hook.
|
|
727
|
+
// Two truncation modes:
|
|
728
|
+
// 1. Partial JSON — first N bytes of valid JSON (check for "success": true)
|
|
729
|
+
// 2. OpenCode replacement — full output replaced with "...N bytes truncated..."
|
|
730
|
+
const successInPartialJson = input.result.match(/"success"\s*:\s*(true|false)/)
|
|
731
|
+
const opencodeTruncation = input.result.match(
|
|
732
|
+
/bytes truncated|output was truncated|tool call succeeded/i,
|
|
733
|
+
)
|
|
734
|
+
const truncatedSuccess = successInPartialJson?.[1] === "true" || !!opencodeTruncation
|
|
735
|
+
if (truncatedSuccess) {
|
|
736
|
+
diag.error(
|
|
737
|
+
"TRUNCATED_OUTPUT",
|
|
738
|
+
`${input.tool} output was truncated (${input.result.length} chars) — tool likely succeeded`,
|
|
739
|
+
)
|
|
740
|
+
logger.warn(
|
|
741
|
+
`Tool output truncated — findings may be incomplete (${input.tool}, ${input.result.length} chars)`,
|
|
742
|
+
)
|
|
743
|
+
completionError = "Tool output truncated — findings may be incomplete"
|
|
744
|
+
} else {
|
|
745
|
+
diag.error("MALFORMED_JSON", `Failed to parse JSON result from ${input.tool}`)
|
|
746
|
+
if (input.tool === "argus_record_finding") {
|
|
747
|
+
throw new Error("argus_record_finding returned malformed JSON")
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
diag.throwIfStrict()
|
|
751
|
+
return
|
|
615
752
|
}
|
|
616
|
-
}
|
|
617
|
-
recordToolExecution(auditState, input.tool, 0)
|
|
618
|
-
onStateChanged?.({ tool: input.tool, findingsCount: 0 })
|
|
619
753
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}),
|
|
628
|
-
)
|
|
629
|
-
}
|
|
754
|
+
const record = toRecord(parsed)
|
|
755
|
+
if (!record) {
|
|
756
|
+
if (input.tool === "argus_record_finding") {
|
|
757
|
+
throw new Error("argus_record_finding response must be a JSON object")
|
|
758
|
+
}
|
|
759
|
+
return
|
|
760
|
+
}
|
|
630
761
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
762
|
+
switch (input.tool) {
|
|
763
|
+
case "argus_slither_analyze": {
|
|
764
|
+
findingsCount = processToolResult(
|
|
765
|
+
record,
|
|
766
|
+
store,
|
|
767
|
+
diag,
|
|
768
|
+
findingMetadata,
|
|
769
|
+
SLITHER_CONFIG,
|
|
770
|
+
projectDir,
|
|
771
|
+
)
|
|
772
|
+
if (auditState.scope.length === 0 && findingsCount > 0) {
|
|
773
|
+
const slitherFindings = Array.isArray(record.findings) ? record.findings : []
|
|
774
|
+
const files = [
|
|
775
|
+
...new Set(
|
|
776
|
+
slitherFindings
|
|
777
|
+
.map((f: Record<string, unknown>) => f.file as string)
|
|
778
|
+
.filter(Boolean),
|
|
779
|
+
),
|
|
780
|
+
]
|
|
781
|
+
if (files.length > 0) {
|
|
782
|
+
auditState.scope = files
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
break
|
|
786
|
+
}
|
|
787
|
+
case "argus_check_patterns":
|
|
788
|
+
findingsCount = processToolResult(
|
|
789
|
+
record,
|
|
790
|
+
store,
|
|
791
|
+
diag,
|
|
792
|
+
findingMetadata,
|
|
793
|
+
PATTERN_CONFIG,
|
|
794
|
+
projectDir,
|
|
795
|
+
)
|
|
796
|
+
if (typeof record.patternVersion === "string") {
|
|
797
|
+
auditState.patternVersion = record.patternVersion
|
|
798
|
+
}
|
|
799
|
+
break
|
|
800
|
+
case "argus_record_finding":
|
|
801
|
+
findingsCount = processToolResult(
|
|
802
|
+
record,
|
|
803
|
+
store,
|
|
804
|
+
diag,
|
|
805
|
+
findingMetadata,
|
|
806
|
+
RECORDED_CONFIG,
|
|
807
|
+
projectDir,
|
|
808
|
+
)
|
|
809
|
+
break
|
|
810
|
+
case "argus_analyze_contract": {
|
|
811
|
+
processContractAnalyzerResult(record, auditState)
|
|
812
|
+
const filePath = (input.args as Record<string, unknown>)?.file_path as string
|
|
813
|
+
if (filePath && !auditState.scope.includes(filePath)) {
|
|
814
|
+
auditState.scope = [...auditState.scope, filePath]
|
|
815
|
+
}
|
|
816
|
+
break
|
|
817
|
+
}
|
|
818
|
+
case "argus_solodit_search":
|
|
819
|
+
processSoloditResult(record, auditState)
|
|
820
|
+
break
|
|
821
|
+
case "argus_forge_test": {
|
|
822
|
+
const summary = toRecord(record.summary)
|
|
823
|
+
if (summary && typeof summary.failed === "number") {
|
|
824
|
+
findingsCount = summary.failed
|
|
825
|
+
}
|
|
826
|
+
break
|
|
827
|
+
}
|
|
828
|
+
case "argus_forge_fuzz":
|
|
829
|
+
processFuzzResult(record, auditState)
|
|
830
|
+
break
|
|
831
|
+
case "argus_generate_report": {
|
|
832
|
+
const reportError = toRecord(record.error)
|
|
833
|
+
const filePath = record.filePath
|
|
834
|
+
if (reportError) {
|
|
835
|
+
const errorMessage =
|
|
836
|
+
typeof reportError.message === "string"
|
|
837
|
+
? reportError.message
|
|
838
|
+
: "argus_generate_report reported an unknown error"
|
|
839
|
+
throw new Error(`argus_generate_report failed: ${errorMessage}`)
|
|
840
|
+
}
|
|
841
|
+
if (typeof filePath !== "string" || filePath.length === 0) {
|
|
842
|
+
throw new Error("argus_generate_report completed without filePath")
|
|
843
|
+
}
|
|
844
|
+
auditState.reportGenerated = true
|
|
845
|
+
break
|
|
846
|
+
}
|
|
847
|
+
case "argus_sync_knowledge": {
|
|
848
|
+
const success = record.success === true
|
|
849
|
+
auditState.knowledgeSynced = { success, timestamp: Date.now() }
|
|
850
|
+
break
|
|
851
|
+
}
|
|
852
|
+
case "argus_forge_coverage": {
|
|
853
|
+
const reportObj = toRecord(record.report)
|
|
854
|
+
const files = reportObj?.files
|
|
855
|
+
if (Array.isArray(files)) {
|
|
856
|
+
auditState.coverageReport = {
|
|
857
|
+
files: files
|
|
858
|
+
.filter((f): f is Record<string, unknown> => !!f && typeof f === "object")
|
|
859
|
+
.map((f) => ({
|
|
860
|
+
path: typeof f.path === "string" ? f.path : "unknown",
|
|
861
|
+
linesPct: typeof f.linesPct === "number" ? f.linesPct : 0,
|
|
862
|
+
statementsPct: typeof f.statementsPct === "number" ? f.statementsPct : 0,
|
|
863
|
+
branchesPct: typeof f.branchesPct === "number" ? f.branchesPct : 0,
|
|
864
|
+
functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
|
|
865
|
+
})),
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
break
|
|
869
|
+
}
|
|
870
|
+
case "argus_proxy_detection": {
|
|
871
|
+
if (record.isProxy === true) {
|
|
872
|
+
auditState.proxyContracts ??= []
|
|
873
|
+
auditState.proxyContracts.push({
|
|
874
|
+
file: typeof record.file === "string" ? record.file : "unknown",
|
|
875
|
+
proxyType: typeof record.proxyType === "string" ? record.proxyType : "unknown",
|
|
876
|
+
indicators: Array.isArray(record.indicators)
|
|
877
|
+
? record.indicators.filter((i): i is string => typeof i === "string")
|
|
878
|
+
: [],
|
|
879
|
+
})
|
|
880
|
+
}
|
|
881
|
+
break
|
|
882
|
+
}
|
|
883
|
+
case "argus_gas_analysis": {
|
|
884
|
+
const hotspots = record.hotspots
|
|
885
|
+
if (Array.isArray(hotspots)) {
|
|
886
|
+
auditState.gasHotspots = hotspots
|
|
887
|
+
.filter((h): h is Record<string, unknown> => !!h && typeof h === "object")
|
|
888
|
+
.map((h) => ({
|
|
889
|
+
contract: typeof h.contract === "string" ? h.contract : "unknown",
|
|
890
|
+
function: typeof h.function === "string" ? h.function : "unknown",
|
|
891
|
+
avgGas: typeof h.avgGas === "number" ? h.avgGas : 0,
|
|
892
|
+
}))
|
|
893
|
+
}
|
|
894
|
+
break
|
|
895
|
+
}
|
|
896
|
+
}
|
|
634
897
|
|
|
635
|
-
|
|
636
|
-
try {
|
|
637
|
-
parsed = JSON.parse(input.result)
|
|
638
|
-
} catch {
|
|
639
|
-
diag.error("MALFORMED_JSON", `Failed to parse JSON result from ${input.tool}`)
|
|
640
|
-
lastDiagnostics = diag.getDiagnostics()
|
|
641
|
-
if (input.tool === "argus_record_finding") {
|
|
642
|
-
throw new Error("argus_record_finding returned malformed JSON")
|
|
643
|
-
}
|
|
644
|
-
diag.throwIfStrict()
|
|
645
|
-
return
|
|
646
|
-
}
|
|
898
|
+
diag.throwIfStrict()
|
|
647
899
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
if (input.tool === "argus_record_finding") {
|
|
652
|
-
throw new Error("argus_record_finding response must be a JSON object")
|
|
653
|
-
}
|
|
654
|
-
return
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
let findingsCount = 0
|
|
900
|
+
if (input.tool === "argus_record_finding" && findingsCount === 0) {
|
|
901
|
+
throw new Error("argus_record_finding did not persist any findings")
|
|
902
|
+
}
|
|
658
903
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
processContractAnalyzerResult(record, auditState)
|
|
671
|
-
break
|
|
672
|
-
case "argus_solodit_search":
|
|
673
|
-
processSoloditResult(record, auditState)
|
|
674
|
-
break
|
|
675
|
-
case "argus_forge_test": {
|
|
676
|
-
const summary = toRecord(record.summary)
|
|
677
|
-
if (summary && typeof summary.failed === "number") {
|
|
678
|
-
findingsCount = summary.failed
|
|
904
|
+
if (input.tool === "argus_record_finding" && !sink) {
|
|
905
|
+
const newFindings = auditState.findings.slice(findingsCountBefore)
|
|
906
|
+
if (newFindings.length > 0) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
`argus_record_finding produced ${newFindings.length} finding(s) but no event sink is available — findings would be lost from the report`,
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
diag.error(
|
|
912
|
+
"NO_EVENT_SINK",
|
|
913
|
+
"argus_record_finding: no active event sink — no new findings to emit",
|
|
914
|
+
)
|
|
679
915
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
path: typeof f.path === "string" ? f.path : "unknown",
|
|
703
|
-
linesPct: typeof f.linesPct === "number" ? f.linesPct : 0,
|
|
704
|
-
statementsPct: typeof f.statementsPct === "number" ? f.statementsPct : 0,
|
|
705
|
-
branchesPct: typeof f.branchesPct === "number" ? f.branchesPct : 0,
|
|
706
|
-
functionsPct: typeof f.functionsPct === "number" ? f.functionsPct : 0,
|
|
707
|
-
})),
|
|
916
|
+
|
|
917
|
+
if (sink) {
|
|
918
|
+
const failFast = input.tool === "argus_record_finding"
|
|
919
|
+
const newFindings = auditState.findings.slice(findingsCountBefore)
|
|
920
|
+
for (const [index, finding] of newFindings.entries()) {
|
|
921
|
+
const { data: canonical } = normalizeToCanonicalFinding(
|
|
922
|
+
finding,
|
|
923
|
+
runId,
|
|
924
|
+
0,
|
|
925
|
+
{
|
|
926
|
+
reportedByAgent,
|
|
927
|
+
reportedBySessionId: sessionId,
|
|
928
|
+
toolCallId,
|
|
929
|
+
observationId: `${toolCallId}:${index + 1}`,
|
|
930
|
+
},
|
|
931
|
+
projectDir,
|
|
932
|
+
)
|
|
933
|
+
await emitToSink(
|
|
934
|
+
sink,
|
|
935
|
+
buildEvent("finding.added", runId, sessionId, toolCallId, canonical),
|
|
936
|
+
{ failFast },
|
|
937
|
+
)
|
|
708
938
|
}
|
|
709
939
|
}
|
|
710
|
-
|
|
940
|
+
|
|
941
|
+
completedSuccess = true
|
|
711
942
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
943
|
+
|
|
944
|
+
recordToolExecution(auditState, input.tool, findingsCount)
|
|
945
|
+
|
|
946
|
+
const nextPhase = inferPhaseAdvancement(auditState, input.tool)
|
|
947
|
+
if (nextPhase) {
|
|
948
|
+
auditState.currentPhase = nextPhase
|
|
949
|
+
if (sink) {
|
|
950
|
+
await emitToSink(
|
|
951
|
+
sink,
|
|
952
|
+
buildEvent("phase.changed", runId, sessionId, toolCallId, {
|
|
953
|
+
phase: nextPhase,
|
|
954
|
+
trigger: input.tool,
|
|
955
|
+
}),
|
|
956
|
+
)
|
|
722
957
|
}
|
|
723
|
-
break
|
|
724
958
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
959
|
+
|
|
960
|
+
onStateChanged?.({ tool: input.tool, findingsCount, sessionId: input.sessionID })
|
|
961
|
+
} catch (error) {
|
|
962
|
+
completionError = error instanceof Error ? error.message : String(error)
|
|
963
|
+
throw error
|
|
964
|
+
} finally {
|
|
965
|
+
lastDiagnostics = diag.getDiagnostics()
|
|
966
|
+
if (sink) {
|
|
967
|
+
const failFast = input.tool === "argus_record_finding"
|
|
968
|
+
// Enrichment data for event replay — projector extracts these from payloads
|
|
969
|
+
const enrichment: Record<string, unknown> = {}
|
|
970
|
+
if (completedSuccess) {
|
|
971
|
+
switch (input.tool) {
|
|
972
|
+
case "argus_solodit_search":
|
|
973
|
+
if (auditState.soloditResults) enrichment.soloditResults = auditState.soloditResults
|
|
974
|
+
break
|
|
975
|
+
case "argus_forge_fuzz":
|
|
976
|
+
if (auditState.fuzzCounterexamples)
|
|
977
|
+
enrichment.fuzzCounterexamples = auditState.fuzzCounterexamples
|
|
978
|
+
break
|
|
979
|
+
case "argus_forge_coverage":
|
|
980
|
+
if (auditState.coverageReport) enrichment.coverageReport = auditState.coverageReport
|
|
981
|
+
break
|
|
982
|
+
case "argus_gas_analysis":
|
|
983
|
+
if (auditState.gasHotspots) enrichment.gasHotspots = auditState.gasHotspots
|
|
984
|
+
break
|
|
985
|
+
case "argus_proxy_detection":
|
|
986
|
+
if (auditState.proxyContracts) enrichment.proxyContracts = auditState.proxyContracts
|
|
987
|
+
break
|
|
988
|
+
case "argus_skill_load":
|
|
989
|
+
if (auditState.skillsLoaded) enrichment.skillsLoaded = auditState.skillsLoaded
|
|
990
|
+
break
|
|
991
|
+
case "argus_check_patterns":
|
|
992
|
+
if (auditState.patternVersion) enrichment.patternVersion = auditState.patternVersion
|
|
993
|
+
break
|
|
994
|
+
}
|
|
735
995
|
}
|
|
736
|
-
|
|
996
|
+
await emitToSink(
|
|
997
|
+
sink,
|
|
998
|
+
buildEvent("tool.completed", runId, sessionId, toolCallId, {
|
|
999
|
+
tool: input.tool,
|
|
1000
|
+
findingsCount,
|
|
1001
|
+
success: completedSuccess,
|
|
1002
|
+
...(completionError ? { error: completionError } : {}),
|
|
1003
|
+
...enrichment,
|
|
1004
|
+
}),
|
|
1005
|
+
{ failFast },
|
|
1006
|
+
)
|
|
1007
|
+
} else if (sessionId) {
|
|
1008
|
+
const enrichment: Record<string, unknown> = {}
|
|
1009
|
+
const event = buildEvent("tool.completed", runId, sessionId, toolCallId, {
|
|
1010
|
+
tool: input.tool,
|
|
1011
|
+
findingsCount,
|
|
1012
|
+
success: completedSuccess,
|
|
1013
|
+
...(completionError ? { error: completionError } : {}),
|
|
1014
|
+
...enrichment,
|
|
1015
|
+
})
|
|
1016
|
+
bufferOrphanEvent(sessionId, {
|
|
1017
|
+
event,
|
|
1018
|
+
failFast: input.tool === "argus_record_finding",
|
|
1019
|
+
bufferedAt: Date.now(),
|
|
1020
|
+
})
|
|
1021
|
+
logger.warn(
|
|
1022
|
+
`Buffered orphan tool.completed event for ${input.tool} from session ${sessionId} (run_id=${runId}, findings=${findingsCount})`,
|
|
1023
|
+
)
|
|
737
1024
|
}
|
|
738
1025
|
}
|
|
1026
|
+
}
|
|
739
1027
|
|
|
740
|
-
|
|
741
|
-
diag.throwIfStrict()
|
|
1028
|
+
hookFn.getLastDiagnostics = (): DropDiagnostic[] => lastDiagnostics
|
|
742
1029
|
|
|
743
|
-
|
|
744
|
-
|
|
1030
|
+
hookFn.flushOrphanEvents = async (sessionId: string, sink: EventSink): Promise<number> => {
|
|
1031
|
+
const entries = orphanBuffer.get(sessionId)
|
|
1032
|
+
if (!entries || entries.length === 0) {
|
|
1033
|
+
return 0
|
|
745
1034
|
}
|
|
746
1035
|
|
|
747
|
-
|
|
748
|
-
|
|
1036
|
+
orphanBuffer.delete(sessionId)
|
|
1037
|
+
const now = Date.now()
|
|
1038
|
+
const fresh = entries.filter((e) => now - e.bufferedAt < ORPHAN_BUFFER_TTL_MS)
|
|
749
1039
|
|
|
750
|
-
if (
|
|
751
|
-
|
|
1040
|
+
if (fresh.length < entries.length) {
|
|
1041
|
+
logger.debug(
|
|
1042
|
+
`Discarded ${entries.length - fresh.length} stale orphan events for session ${sessionId}`,
|
|
1043
|
+
)
|
|
752
1044
|
}
|
|
753
1045
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
reportedByAgent,
|
|
760
|
-
reportedBySessionId: sessionId,
|
|
761
|
-
toolCallId,
|
|
762
|
-
observationId: `${toolCallId}:${index + 1}`,
|
|
763
|
-
})
|
|
764
|
-
await emitToSink(
|
|
765
|
-
sink,
|
|
766
|
-
buildEvent("finding.added", runId, sessionId, toolCallId, canonical),
|
|
767
|
-
{ failFast },
|
|
768
|
-
)
|
|
769
|
-
}
|
|
1046
|
+
let flushed = 0
|
|
1047
|
+
for (const entry of fresh) {
|
|
1048
|
+
await emitToSink(sink, entry.event, { failFast: entry.failFast })
|
|
1049
|
+
flushed++
|
|
1050
|
+
}
|
|
770
1051
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
buildEvent("tool.completed", runId, sessionId, toolCallId, {
|
|
774
|
-
tool: input.tool,
|
|
775
|
-
findingsCount,
|
|
776
|
-
success: true,
|
|
777
|
-
}),
|
|
778
|
-
{ failFast },
|
|
779
|
-
)
|
|
1052
|
+
if (flushed > 0) {
|
|
1053
|
+
logger.info(`Flushed ${flushed} orphan events for session ${sessionId} to sink ${sink.runId}`)
|
|
780
1054
|
}
|
|
781
|
-
}
|
|
782
1055
|
|
|
783
|
-
|
|
1056
|
+
return flushed
|
|
1057
|
+
}
|
|
784
1058
|
|
|
785
1059
|
return hookFn
|
|
786
1060
|
}
|