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
|
@@ -3,15 +3,21 @@ import path from "node:path"
|
|
|
3
3
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
4
4
|
import { loadArgusConfig } from "../config/loader"
|
|
5
5
|
import type { ArgusConfig } from "../config/types"
|
|
6
|
+
import { readEvents } from "../features/persistent-state/event-sink"
|
|
6
7
|
import type { DropDiagnostic, DropPolicy } from "../shared/drop-diagnostics"
|
|
7
8
|
import { createDropDiagnosticsCollector } from "../shared/drop-diagnostics"
|
|
8
9
|
import { createLogger } from "../shared/logger"
|
|
9
10
|
import { resolveProjectDir } from "../shared/project-utils"
|
|
10
11
|
import { resolveReportPath } from "../shared/report-path-resolver"
|
|
11
12
|
import { normalizeToCanonicalFinding } from "../state/adapters"
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
compareIssueFingerprintSets,
|
|
15
|
+
dedupeFindingsForFinalOutput,
|
|
16
|
+
} from "../state/finding-aggregation"
|
|
17
|
+
import { projectFindings, stableHash } from "../state/projectors"
|
|
13
18
|
import { type ReportInput, SCHEMA_VERSION, validateReportInput } from "../state/schemas"
|
|
14
19
|
import type { AuditState, Finding, FindingSeverity } from "../state/types"
|
|
20
|
+
import { checkReportPreflight } from "./report-preflight"
|
|
15
21
|
|
|
16
22
|
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
|
|
17
23
|
|
|
@@ -23,6 +29,7 @@ type ReportGeneratorArgs = {
|
|
|
23
29
|
quality_gate_policy?: QualityGatePolicy
|
|
24
30
|
report_input?: string
|
|
25
31
|
audit_state?: string
|
|
32
|
+
preflight_policy?: PreflightPolicy
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
type FindingsCount = {
|
|
@@ -46,6 +53,8 @@ export type ReportGenerationResult = {
|
|
|
46
53
|
|
|
47
54
|
type QualityGatePolicy = "warn" | "strict-fail"
|
|
48
55
|
|
|
56
|
+
type PreflightPolicy = "warn" | "strict-fail"
|
|
57
|
+
|
|
49
58
|
type ReportQualityViolation = {
|
|
50
59
|
findingId: string
|
|
51
60
|
code: string
|
|
@@ -59,6 +68,10 @@ type ReportQualityValidation = {
|
|
|
59
68
|
|
|
60
69
|
export type ReportGenerationDependencies = {
|
|
61
70
|
loadConfig?: (projectDir: string) => ArgusConfig
|
|
71
|
+
readEvents?: (
|
|
72
|
+
runId: string,
|
|
73
|
+
projectDir: string,
|
|
74
|
+
) => Promise<import("../state/schemas").AuditEvent[]>
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
export const SINGLE_WRITER_POLICY_VERSION = "1.0.0"
|
|
@@ -993,9 +1006,25 @@ export function buildProvenanceAppendix(
|
|
|
993
1006
|
lines.push("| Tool | Duration | Status | Findings |")
|
|
994
1007
|
lines.push("| --- | --- | --- | ---: |")
|
|
995
1008
|
for (const exec of state.toolsExecuted) {
|
|
996
|
-
const
|
|
997
|
-
const
|
|
998
|
-
|
|
1009
|
+
const toolName = typeof exec.tool === "string" && exec.tool ? exec.tool : "(unknown tool)"
|
|
1010
|
+
const hasTimes =
|
|
1011
|
+
typeof exec.startTime === "number" &&
|
|
1012
|
+
!Number.isNaN(exec.startTime) &&
|
|
1013
|
+
exec.endTime != null &&
|
|
1014
|
+
typeof exec.endTime === "number" &&
|
|
1015
|
+
!Number.isNaN(exec.endTime)
|
|
1016
|
+
const duration = hasTimes ? formatDuration((exec.endTime as number) - exec.startTime) : "N/A"
|
|
1017
|
+
const status =
|
|
1018
|
+
typeof exec.success === "boolean"
|
|
1019
|
+
? exec.success
|
|
1020
|
+
? "\u2705 success"
|
|
1021
|
+
: "\u274C failure"
|
|
1022
|
+
: "\u26A0 malformed"
|
|
1023
|
+
const findings =
|
|
1024
|
+
typeof exec.findingsCount === "number" && !Number.isNaN(exec.findingsCount)
|
|
1025
|
+
? exec.findingsCount
|
|
1026
|
+
: "N/A"
|
|
1027
|
+
lines.push(`| ${toolName} | ${duration} | ${status} | ${findings} |`)
|
|
999
1028
|
}
|
|
1000
1029
|
}
|
|
1001
1030
|
|
|
@@ -1008,7 +1037,11 @@ export function buildProvenanceAppendix(
|
|
|
1008
1037
|
lines.push(`- Pattern pack version: \`${state.patternVersion}\``)
|
|
1009
1038
|
}
|
|
1010
1039
|
if (syncExec) {
|
|
1011
|
-
|
|
1040
|
+
const syncTime =
|
|
1041
|
+
typeof syncExec.startTime === "number" && !Number.isNaN(syncExec.startTime)
|
|
1042
|
+
? new Date(syncExec.startTime).toISOString()
|
|
1043
|
+
: "N/A"
|
|
1044
|
+
lines.push(`- SCVD last synced: ${syncTime}`)
|
|
1012
1045
|
}
|
|
1013
1046
|
}
|
|
1014
1047
|
|
|
@@ -1066,10 +1099,81 @@ export async function executeReportGeneration(
|
|
|
1066
1099
|
const threshold = args.severity_threshold ?? "low"
|
|
1067
1100
|
const qualityGatePolicy = args.quality_gate_policy ?? "warn"
|
|
1068
1101
|
const { reportInput, diagnostics } = parseReportInputPayload(args, context)
|
|
1102
|
+
const preflightPolicy = args.preflight_policy ?? "warn"
|
|
1103
|
+
let preflightWarningSection: string | null = null
|
|
1104
|
+
const warningBullets: string[] = []
|
|
1105
|
+
try {
|
|
1106
|
+
const readEventsFn = deps.readEvents ?? readEvents
|
|
1107
|
+
const events = await readEventsFn(reportInput.run_id, reportInput.projectDir)
|
|
1108
|
+
const preflightResult = checkReportPreflight(events)
|
|
1109
|
+
if (!preflightResult.passed) {
|
|
1110
|
+
if (preflightPolicy === "strict-fail") {
|
|
1111
|
+
const parts: string[] = []
|
|
1112
|
+
if (preflightResult.orphanedTools.length > 0)
|
|
1113
|
+
parts.push(`orphaned tools: ${preflightResult.orphanedTools.join(", ")}`)
|
|
1114
|
+
if (preflightResult.missingLifecycle.length > 0)
|
|
1115
|
+
parts.push(`missing lifecycle: ${preflightResult.missingLifecycle.join(", ")}`)
|
|
1116
|
+
if (preflightResult.missingRequiredTools.length > 0)
|
|
1117
|
+
parts.push(`missing required tools: ${preflightResult.missingRequiredTools.join(", ")}`)
|
|
1118
|
+
throw new Error(`Preflight failed (strict-fail): ${parts.join("; ")}`)
|
|
1119
|
+
}
|
|
1120
|
+
if (preflightResult.orphanedTools.length > 0)
|
|
1121
|
+
warningBullets.push(`- Orphaned tools: ${preflightResult.orphanedTools.join(", ")}`)
|
|
1122
|
+
if (preflightResult.missingLifecycle.length > 0)
|
|
1123
|
+
warningBullets.push(`- Missing lifecycle: ${preflightResult.missingLifecycle.join(", ")}`)
|
|
1124
|
+
if (preflightResult.missingRequiredTools.length > 0)
|
|
1125
|
+
warningBullets.push(
|
|
1126
|
+
`- Missing required tools: ${preflightResult.missingRequiredTools.join(", ")}`,
|
|
1127
|
+
)
|
|
1128
|
+
if (preflightResult.warnings.length > 0)
|
|
1129
|
+
warningBullets.push(`- Warnings: ${preflightResult.warnings.join(", ")}`)
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const eventFindings = dedupeFindingsForFinalOutput(projectFindings(events))
|
|
1133
|
+
const inputFindings = dedupeFindingsForFinalOutput(reportInput.findings)
|
|
1134
|
+
const parity = compareIssueFingerprintSets(eventFindings, inputFindings)
|
|
1135
|
+
|
|
1136
|
+
if (!parity.matches) {
|
|
1137
|
+
const mismatchSummary = `missing=${parity.missing.length}, extra=${parity.extra.length}`
|
|
1138
|
+
if (preflightPolicy === "strict-fail") {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
`Preflight failed (strict-fail): finding parity mismatch (${mismatchSummary})`,
|
|
1141
|
+
)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
warningBullets.push(`- Finding parity mismatch: ${mismatchSummary}`)
|
|
1145
|
+
if (parity.missing.length > 0) {
|
|
1146
|
+
warningBullets.push(`- Missing issue fingerprints: ${parity.missing.join(", ")}`)
|
|
1147
|
+
}
|
|
1148
|
+
if (parity.extra.length > 0) {
|
|
1149
|
+
warningBullets.push(`- Extra issue fingerprints: ${parity.extra.join(", ")}`)
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
if (err instanceof Error && err.message.startsWith("Preflight failed (strict-fail)")) {
|
|
1154
|
+
throw err
|
|
1155
|
+
}
|
|
1156
|
+
if (preflightPolicy === "strict-fail") {
|
|
1157
|
+
throw new Error("Preflight failed: unable to read event stream for completeness check")
|
|
1158
|
+
}
|
|
1159
|
+
// warn mode: skip preflight when events cannot be read
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (warningBullets.length > 0) {
|
|
1163
|
+
preflightWarningSection = [
|
|
1164
|
+
"## \u26A0 Completeness Warning",
|
|
1165
|
+
"",
|
|
1166
|
+
"This report was generated with incomplete orchestration state.",
|
|
1167
|
+
"",
|
|
1168
|
+
...warningBullets,
|
|
1169
|
+
].join("\n")
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1069
1172
|
const state = reportInputToAuditState(reportInput)
|
|
1070
1173
|
const scope = args.scope.length > 0 ? args.scope : reportInput.scope
|
|
1174
|
+
const finalFindings = dedupeFindingsForFinalOutput(reportInput.findings)
|
|
1071
1175
|
const findings = sortFindingsDeterministically(
|
|
1072
|
-
|
|
1176
|
+
finalFindings.filter((finding) => shouldIncludeFinding(finding, threshold)),
|
|
1073
1177
|
)
|
|
1074
1178
|
const qualityGates = validateReportQuality(findings, qualityGatePolicy)
|
|
1075
1179
|
if (!qualityGates.passed && qualityGatePolicy === "strict-fail") {
|
|
@@ -1129,6 +1233,10 @@ export async function executeReportGeneration(
|
|
|
1129
1233
|
sections.push(`- ${item}`)
|
|
1130
1234
|
}
|
|
1131
1235
|
|
|
1236
|
+
if (preflightWarningSection) {
|
|
1237
|
+
sections.push(preflightWarningSection)
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1132
1240
|
sections.push(buildProvenanceAppendix(state, threshold, findings.length))
|
|
1133
1241
|
|
|
1134
1242
|
// Embed report metadata for single-writer policy enforcement
|
|
@@ -1159,7 +1267,7 @@ export async function executeReportGeneration(
|
|
|
1159
1267
|
const loadConfig = deps.loadConfig ?? loadArgusConfig
|
|
1160
1268
|
const projectDir = resolveProjectDir(context)
|
|
1161
1269
|
const config = loadConfig(projectDir)
|
|
1162
|
-
const outputDir = config.reporting?.output_dir ?? ".
|
|
1270
|
+
const outputDir = config.reporting?.output_dir ?? ".argus/reports/"
|
|
1163
1271
|
const fullPath = path.join(projectDir, outputDir, canonicalFilename)
|
|
1164
1272
|
|
|
1165
1273
|
// Single-writer policy: check for duplicate writes with same run_id
|
|
@@ -1194,6 +1302,7 @@ export const reportGeneratorTool = tool({
|
|
|
1194
1302
|
.default("low"),
|
|
1195
1303
|
report_input: tool.schema.string().optional(),
|
|
1196
1304
|
audit_state: tool.schema.string().optional(),
|
|
1305
|
+
preflight_policy: tool.schema.enum(["warn", "strict-fail"]).optional(),
|
|
1197
1306
|
},
|
|
1198
1307
|
async execute(args, context) {
|
|
1199
1308
|
const result = await executeReportGeneration(args, context)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectToolLifecycleIssues,
|
|
3
|
+
hasSessionCreated,
|
|
4
|
+
hasSessionDeleted,
|
|
5
|
+
} from "../features/persistent-state/run-finalizer"
|
|
6
|
+
import type { AuditEvent } from "../state/schemas"
|
|
7
|
+
|
|
8
|
+
export interface PreflightResult {
|
|
9
|
+
passed: boolean
|
|
10
|
+
orphanedTools: string[]
|
|
11
|
+
missingLifecycle: string[]
|
|
12
|
+
missingRequiredTools: string[]
|
|
13
|
+
warnings: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PreflightOptions {
|
|
17
|
+
requiredTools?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
21
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
22
|
+
return value as Record<string, unknown>
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasCompletedTool(events: AuditEvent[], toolName: string): boolean {
|
|
28
|
+
for (const event of events) {
|
|
29
|
+
if (event.type !== "tool.completed") {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const payload = asRecord(event.payload)
|
|
34
|
+
if (!payload) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const name = payload.name
|
|
39
|
+
const tool = payload.tool
|
|
40
|
+
if (name === toolName || tool === toolName) {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function checkReportPreflight(
|
|
49
|
+
events: AuditEvent[],
|
|
50
|
+
options: PreflightOptions = {},
|
|
51
|
+
): PreflightResult {
|
|
52
|
+
const missingLifecycle: string[] = []
|
|
53
|
+
if (!hasSessionCreated(events)) {
|
|
54
|
+
missingLifecycle.push("session.created")
|
|
55
|
+
}
|
|
56
|
+
if (!hasSessionDeleted(events)) {
|
|
57
|
+
missingLifecycle.push("session.deleted")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { orphanedToolCallIds, malformedEvents } = collectToolLifecycleIssues(events)
|
|
61
|
+
|
|
62
|
+
const missingRequiredTools: string[] = []
|
|
63
|
+
for (const requiredTool of options.requiredTools ?? []) {
|
|
64
|
+
if (!hasCompletedTool(events, requiredTool)) {
|
|
65
|
+
missingRequiredTools.push(requiredTool)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
passed:
|
|
71
|
+
orphanedToolCallIds.length === 0 &&
|
|
72
|
+
missingLifecycle.length === 0 &&
|
|
73
|
+
missingRequiredTools.length === 0,
|
|
74
|
+
orphanedTools: orphanedToolCallIds,
|
|
75
|
+
missingLifecycle,
|
|
76
|
+
missingRequiredTools,
|
|
77
|
+
warnings: malformedEvents,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -258,7 +258,9 @@ export async function executeSoloditSearch(
|
|
|
258
258
|
let hadMcpError = false
|
|
259
259
|
for (const toolName of SOLODIT_MCP_TOOLS) {
|
|
260
260
|
try {
|
|
261
|
-
logger.debug(
|
|
261
|
+
logger.debug(
|
|
262
|
+
`[solodit] Trying MCP tool '${toolName}' on server '${SOLODIT_MCP_SERVER}' for query: ${query}`,
|
|
263
|
+
)
|
|
262
264
|
const response = await mcpCaller(
|
|
263
265
|
SOLODIT_MCP_SERVER,
|
|
264
266
|
toolName,
|
|
@@ -289,7 +291,9 @@ export async function executeSoloditSearch(
|
|
|
289
291
|
}
|
|
290
292
|
|
|
291
293
|
// All MCP tools failed — fall back to HTTP
|
|
292
|
-
logger.debug(
|
|
294
|
+
logger.debug(
|
|
295
|
+
`[solodit] All MCP tools failed (hadMcpError=${hadMcpError}) — falling back to HTTP for query: ${query}`,
|
|
296
|
+
)
|
|
293
297
|
return callSoloditHttp(query, limit, args.severity, port)
|
|
294
298
|
}
|
|
295
299
|
|