solidity-argus 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solidity-argus",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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",
@@ -41,6 +41,31 @@ export type AgentTrackerRef = {
41
41
 
42
42
  let _agentTrackerRef: AgentTrackerRef | undefined
43
43
 
44
+ const REPORT_METADATA_REGEX = /<!-- argus:report_metadata (.+?) -->/
45
+
46
+ function extractRunIdFromReportToolOutput(result: string): string | undefined {
47
+ try {
48
+ const parsed = JSON.parse(result) as Record<string, unknown>
49
+ if (typeof parsed.run_id === "string" && parsed.run_id.length > 0) {
50
+ return parsed.run_id
51
+ }
52
+
53
+ if (typeof parsed.report === "string") {
54
+ const match = parsed.report.match(REPORT_METADATA_REGEX)
55
+ if (match?.[1]) {
56
+ const metadata = JSON.parse(match[1]) as Record<string, unknown>
57
+ if (typeof metadata.run_id === "string" && metadata.run_id.length > 0) {
58
+ return metadata.run_id
59
+ }
60
+ }
61
+ }
62
+ } catch {
63
+ return undefined
64
+ }
65
+
66
+ return undefined
67
+ }
68
+
44
69
  export function getAgentForSession(sessionID: string): string | undefined {
45
70
  return _agentTrackerRef?.getAgentForSession(sessionID)
46
71
  }
@@ -326,6 +351,20 @@ export function createHooks(args: {
326
351
  return agent
327
352
  }
328
353
 
354
+ return "unknown"
355
+ },
356
+ getAgentNameForSession: (sessionId: string) => {
357
+ const agent = agentTracker.getAgentForSession(sessionId)
358
+ if (
359
+ agent === "argus" ||
360
+ agent === "sentinel" ||
361
+ agent === "pythia" ||
362
+ agent === "scribe" ||
363
+ agent === "unknown"
364
+ ) {
365
+ return agent
366
+ }
367
+
329
368
  return "unknown"
330
369
  },
331
370
  },
@@ -334,30 +373,31 @@ export function createHooks(args: {
334
373
  )
335
374
  : undefined
336
375
 
337
- const materializeCurrentFindings = async (
376
+ const materializeFindingsForRun = async (
377
+ runId: string,
378
+ projectDirForRun: string,
379
+ sessionIdForRun: string | undefined,
338
380
  trigger: "session.idle" | "tool.execute.after",
339
381
  failFast = false,
340
382
  ): Promise<void> => {
341
- const state = getAuditState()
342
- if (!state || state.sessionId.length === 0) {
383
+ if (!runId || runId.length === 0) {
343
384
  return
344
385
  }
345
386
 
346
387
  try {
347
- await materializeFindings(
348
- state.sessionId,
349
- state.projectDir,
350
- currentOpencodeSessionId.length > 0 ? currentOpencodeSessionId : undefined,
351
- )
388
+ await materializeFindings(runId, projectDirForRun, sessionIdForRun, {
389
+ validateSessionId: sessionIdForRun != null && sessionIdForRun.length > 0,
390
+ requireEvents: true,
391
+ })
352
392
  } catch (error) {
353
393
  if (failFast) {
354
394
  throw new Error(
355
- `Failed to materialize findings artifact on ${trigger} for run ${state.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
395
+ `Failed to materialize findings artifact on ${trigger} for run ${runId}: ${error instanceof Error ? error.message : String(error)}`,
356
396
  )
357
397
  }
358
398
 
359
399
  logger.warn(
360
- `Failed to materialize findings artifact on ${trigger} for run ${state.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
400
+ `Failed to materialize findings artifact on ${trigger} for run ${runId}: ${error instanceof Error ? error.message : String(error)}`,
361
401
  )
362
402
  }
363
403
  }
@@ -378,7 +418,13 @@ export function createHooks(args: {
378
418
 
379
419
  if (hasNewFinalization && finalizationResult.runId.length > 0) {
380
420
  try {
381
- await materializeFindings(finalizationResult.runId, projectDir)
421
+ await materializeFindingsForRun(
422
+ finalizationResult.runId,
423
+ projectDir,
424
+ input.event.sessionId,
425
+ "session.idle",
426
+ true,
427
+ )
382
428
  } catch (error) {
383
429
  logger.warn(
384
430
  `Failed to materialize findings artifact for run ${finalizationResult.runId}: ${error instanceof Error ? error.message : String(error)}`,
@@ -437,10 +483,24 @@ export function createHooks(args: {
437
483
  tool: input.tool,
438
484
  args: input.args,
439
485
  result: output.output,
486
+ sessionID: input.sessionID,
487
+ callID: input.callID,
440
488
  })
441
489
 
442
490
  if (input.tool === "argus_generate_report") {
443
- await materializeCurrentFindings("tool.execute.after", true)
491
+ const state = getAuditState()
492
+ if (!state || state.sessionId.length === 0) {
493
+ throw new Error("argus_generate_report completed without active audit state")
494
+ }
495
+
496
+ const runId = extractRunIdFromReportToolOutput(output.output) ?? state.sessionId
497
+ await materializeFindingsForRun(
498
+ runId,
499
+ state.projectDir,
500
+ input.sessionID,
501
+ "tool.execute.after",
502
+ true,
503
+ )
444
504
  }
445
505
 
446
506
  const outputWithHint = recoveryHint ? `${output.output}${recoveryHint}` : output.output
@@ -20,12 +20,34 @@ export interface FindingsArtifact {
20
20
  toolsExecuted: CanonicalToolExecution[]
21
21
  }
22
22
 
23
+ export interface FindingsMaterializeOptions {
24
+ validateSessionId?: boolean
25
+ requireEvents?: boolean
26
+ }
27
+
23
28
  export async function materializeFindings(
24
29
  runId: string,
25
30
  projectDir: string,
26
31
  sessionId?: string,
32
+ options: FindingsMaterializeOptions = {},
27
33
  ): Promise<FindingsArtifact> {
28
34
  const events = await readEvents(runId, projectDir)
35
+ if (options.requireEvents && events.length === 0) {
36
+ throw new Error(`No events found for run ${runId}`)
37
+ }
38
+
39
+ const sessionIdFromEvents = events[0]?.session_id ?? ""
40
+ if (
41
+ options.validateSessionId &&
42
+ sessionId &&
43
+ sessionIdFromEvents.length > 0 &&
44
+ sessionId !== sessionIdFromEvents
45
+ ) {
46
+ throw new Error(
47
+ `Session mismatch for run ${runId}: provided ${sessionId}, event stream has ${sessionIdFromEvents}`,
48
+ )
49
+ }
50
+
29
51
  const findings = dedupeFindingsForFinalOutput(projectFindings(events))
30
52
  const toolsExecuted = projectToolExecutions(events)
31
53
  const contentHash = stableHash(JSON.stringify(findings))
@@ -33,7 +55,7 @@ export async function materializeFindings(
33
55
 
34
56
  const artifact: FindingsArtifact = {
35
57
  run_id: runId,
36
- session_id: sessionId ?? events[0]?.session_id ?? "",
58
+ session_id: sessionId ?? sessionIdFromEvents,
37
59
  schema_version: SCHEMA_VERSION,
38
60
  seq_first: events[0]?.seq ?? 0,
39
61
  seq_last: events.at(-1)?.seq ?? 0,
@@ -113,7 +113,6 @@ export function createEventHook(
113
113
 
114
114
  case "session.deleted": {
115
115
  preDeleteState = currentAuditState
116
- currentAuditState = null
117
116
  break
118
117
  }
119
118
 
@@ -179,6 +178,7 @@ export function createEventHook(
179
178
  }
180
179
  }
181
180
  }
181
+ currentAuditState = null
182
182
  eventSink = null
183
183
  break
184
184
  }
@@ -27,6 +27,8 @@ type ToolHookInput = {
27
27
  tool: string
28
28
  args: unknown
29
29
  result: string
30
+ sessionID?: string
31
+ callID?: string
30
32
  }
31
33
 
32
34
  type ToolExecutionMetadata = {
@@ -38,6 +40,7 @@ export type ToolTrackingOptions = {
38
40
  getEventSink?: () => EventSink | null
39
41
  getSessionId?: () => string
40
42
  getAgentName?: () => ArgusAgentName | undefined
43
+ getAgentNameForSession?: (sessionId: string) => ArgusAgentName | undefined
41
44
  dropPolicy?: DropPolicy
42
45
  onChildSessionDetected?: (parentSessionId: string, childSessionId: string) => void
43
46
  }
@@ -502,7 +505,7 @@ export function createToolTrackingHook(
502
505
  const correlationId = randomUUID()
503
506
  const resolved = resolveStateAndStore()
504
507
  const sink = options?.getEventSink?.()
505
- const sessionId = options?.getSessionId?.() ?? ""
508
+ const sessionId = input.sessionID ?? options?.getSessionId?.() ?? ""
506
509
  const toolCallId = randomUUID()
507
510
 
508
511
  if (childSessionId) {
@@ -576,8 +579,11 @@ export function createToolTrackingHook(
576
579
  const { state: auditState, store } = resolved
577
580
  const sink = options?.getEventSink?.()
578
581
  const runId = auditState.sessionId
579
- const sessionId = options?.getSessionId?.() ?? ""
580
- const reportedByAgent = options?.getAgentName?.() ?? "unknown"
582
+ const sessionId = input.sessionID ?? options?.getSessionId?.() ?? ""
583
+ const reportedByAgent =
584
+ (input.sessionID ? options?.getAgentNameForSession?.(input.sessionID) : undefined) ??
585
+ options?.getAgentName?.() ??
586
+ "unknown"
581
587
  const findingMetadata = {
582
588
  reportedByAgent,
583
589
  reportedBySessionId: sessionId,
@@ -44,6 +44,7 @@ export type ReportGenerationResult = {
44
44
  report: string
45
45
  findingsCount: FindingsCount
46
46
  filename: string
47
+ run_id: string
47
48
  contentHash: string
48
49
  qualityGates: ReportQualityValidation
49
50
  contractDiagnostics: DropDiagnostic[]
@@ -1258,6 +1259,7 @@ export async function executeReportGeneration(
1258
1259
  report: reportMarkdown,
1259
1260
  findingsCount: counts,
1260
1261
  filename: canonicalFilename,
1262
+ run_id: runId,
1261
1263
  contentHash,
1262
1264
  qualityGates,
1263
1265
  contractDiagnostics: diagnostics,