solidity-argus 0.3.6 → 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 +851 -142
- 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 +57 -3
- 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 +606 -326
- 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 +396 -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
package/src/hooks/event-hook.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { EventSink } from "../features/persistent-state/event-sink"
|
|
2
|
+
import { updateRunStatus } from "../features/persistent-state/global-run-index"
|
|
2
3
|
import type { FinalizationResult } from "../features/persistent-state/run-finalizer"
|
|
3
4
|
import { finalizeRun } from "../features/persistent-state/run-finalizer"
|
|
4
5
|
import { createLogger } from "../shared/logger"
|
|
5
6
|
import { ARGUS_PLUGIN_VERSION } from "../shared/plugin-metadata"
|
|
7
|
+
import { safeEmitToSink } from "../shared/safe-emit"
|
|
6
8
|
import { createAuditState } from "../state/audit-state"
|
|
7
9
|
import type { AuditEvent } from "../state/schemas"
|
|
8
10
|
import { SCHEMA_VERSION } from "../state/schemas"
|
|
@@ -18,9 +20,41 @@ export type AuditEventType =
|
|
|
18
20
|
| "audit.complete"
|
|
19
21
|
|
|
20
22
|
export type EventHookFn = (input: {
|
|
21
|
-
event: { type: string;
|
|
23
|
+
event: { type: string; properties?: Record<string, unknown> }
|
|
22
24
|
}) => Promise<void>
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Extract the OpenCode session ID from an SDK Event object.
|
|
28
|
+
*
|
|
29
|
+
* The OpenCode SDK Event union uses different shapes depending on event type:
|
|
30
|
+
* - session.created / session.deleted → { properties: { info: { id: string } } }
|
|
31
|
+
* - session.idle / session.error → { properties: { sessionID: string } }
|
|
32
|
+
* - Other events may have properties.sessionID or none at all.
|
|
33
|
+
*/
|
|
34
|
+
export function extractSessionId(event: {
|
|
35
|
+
type: string
|
|
36
|
+
properties?: Record<string, unknown>
|
|
37
|
+
}): string | undefined {
|
|
38
|
+
const props = event.properties
|
|
39
|
+
if (!props) return undefined
|
|
40
|
+
|
|
41
|
+
// session.idle, session.error, and many other events use properties.sessionID
|
|
42
|
+
if (typeof props.sessionID === "string" && props.sessionID.length > 0) {
|
|
43
|
+
return props.sessionID
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// session.created and session.deleted wrap a Session object at properties.info
|
|
47
|
+
const info = props.info
|
|
48
|
+
if (info && typeof info === "object" && info !== null) {
|
|
49
|
+
const infoRecord = info as Record<string, unknown>
|
|
50
|
+
if (typeof infoRecord.id === "string" && infoRecord.id.length > 0) {
|
|
51
|
+
return infoRecord.id
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
export type EventSubHandler = (event: {
|
|
25
59
|
type: string
|
|
26
60
|
sessionId?: string
|
|
@@ -33,78 +67,158 @@ export function createEventHook(
|
|
|
33
67
|
subHandlers: EventSubHandler[] = [],
|
|
34
68
|
): {
|
|
35
69
|
hook: EventHookFn
|
|
36
|
-
getAuditState: () => AuditState | null
|
|
37
|
-
setAuditState: (state: AuditState | null) => void
|
|
38
|
-
setEventSink: (sink: EventSink | null) => void
|
|
70
|
+
getAuditState: (sessionId?: string) => AuditState | null
|
|
71
|
+
setAuditState: (state: AuditState | null, sessionId?: string) => void
|
|
72
|
+
setEventSink: (sink: EventSink | null, sessionId?: string) => void
|
|
73
|
+
getEventSink: (sessionId?: string) => EventSink | null
|
|
39
74
|
getLastFinalizationResult: () => FinalizationResult | null
|
|
40
75
|
} {
|
|
41
76
|
const logger = createLogger()
|
|
42
|
-
|
|
43
|
-
|
|
77
|
+
const statesBySessionId = new Map<string, AuditState>()
|
|
78
|
+
const sinksBySessionId = new Map<string, EventSink>()
|
|
79
|
+
|
|
80
|
+
const MAX_SESSION_STATES = 500
|
|
81
|
+
|
|
82
|
+
function setSessionState(sessionId: string, state: AuditState): void {
|
|
83
|
+
if (statesBySessionId.size >= MAX_SESSION_STATES && !statesBySessionId.has(sessionId)) {
|
|
84
|
+
const oldest = statesBySessionId.keys().next().value
|
|
85
|
+
if (oldest) statesBySessionId.delete(oldest)
|
|
86
|
+
}
|
|
87
|
+
statesBySessionId.set(sessionId, state)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function setSessionSink(sessionId: string, sink: EventSink): void {
|
|
91
|
+
if (sinksBySessionId.size >= MAX_SESSION_STATES && !sinksBySessionId.has(sessionId)) {
|
|
92
|
+
const oldest = sinksBySessionId.keys().next().value
|
|
93
|
+
if (oldest) sinksBySessionId.delete(oldest)
|
|
94
|
+
}
|
|
95
|
+
sinksBySessionId.set(sessionId, sink)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let fallbackAuditState: AuditState | null = null
|
|
99
|
+
let fallbackEventSink: EventSink | null = null
|
|
100
|
+
let activeSessionId = ""
|
|
44
101
|
let lastFinalizationResult: FinalizationResult | null = null
|
|
45
102
|
|
|
46
|
-
const getAuditState = (): AuditState | null =>
|
|
47
|
-
|
|
48
|
-
|
|
103
|
+
const getAuditState = (sessionId?: string): AuditState | null => {
|
|
104
|
+
if (sessionId && sessionId.length > 0) {
|
|
105
|
+
const sessionState = statesBySessionId.get(sessionId)
|
|
106
|
+
if (sessionState) {
|
|
107
|
+
return sessionState
|
|
108
|
+
}
|
|
109
|
+
// Fall through to activeSessionId — child sessions (e.g. sentinel)
|
|
110
|
+
// may not have their own state entry but share the parent's state.
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (activeSessionId.length > 0) {
|
|
114
|
+
return statesBySessionId.get(activeSessionId) ?? fallbackAuditState
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return fallbackAuditState
|
|
49
118
|
}
|
|
50
|
-
|
|
51
|
-
|
|
119
|
+
|
|
120
|
+
const setAuditState = (state: AuditState | null, sessionId?: string): void => {
|
|
121
|
+
if (sessionId && sessionId.length > 0) {
|
|
122
|
+
if (state) {
|
|
123
|
+
setSessionState(sessionId, state)
|
|
124
|
+
activeSessionId = sessionId
|
|
125
|
+
} else {
|
|
126
|
+
statesBySessionId.delete(sessionId)
|
|
127
|
+
}
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fallbackAuditState = state
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const getEventSink = (sessionId?: string): EventSink | null => {
|
|
135
|
+
if (sessionId && sessionId.length > 0) {
|
|
136
|
+
const sessionSink = sinksBySessionId.get(sessionId)
|
|
137
|
+
if (sessionSink) {
|
|
138
|
+
return sessionSink
|
|
139
|
+
}
|
|
140
|
+
return fallbackEventSink
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (activeSessionId.length > 0) {
|
|
144
|
+
return sinksBySessionId.get(activeSessionId) ?? fallbackEventSink
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return fallbackEventSink
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const setEventSink = (sink: EventSink | null, sessionId?: string): void => {
|
|
151
|
+
if (sessionId && sessionId.length > 0) {
|
|
152
|
+
if (sink) {
|
|
153
|
+
setSessionSink(sessionId, sink)
|
|
154
|
+
} else {
|
|
155
|
+
sinksBySessionId.delete(sessionId)
|
|
156
|
+
}
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fallbackEventSink = sink
|
|
52
161
|
}
|
|
53
162
|
|
|
54
163
|
async function emitToSink(
|
|
164
|
+
sink: EventSink | null,
|
|
55
165
|
type: AuditEvent["type"],
|
|
56
166
|
runId: string,
|
|
57
167
|
sessionId: string | undefined,
|
|
58
168
|
payload: unknown,
|
|
59
169
|
): Promise<void> {
|
|
60
|
-
if (!
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})
|
|
72
|
-
} catch (error) {
|
|
73
|
-
logger.error(
|
|
74
|
-
`Failed to emit ${type} event to sink: ${error instanceof Error ? error.message : String(error)}`,
|
|
75
|
-
)
|
|
76
|
-
}
|
|
170
|
+
if (!sink) return
|
|
171
|
+
await safeEmitToSink(sink, {
|
|
172
|
+
type,
|
|
173
|
+
run_id: runId,
|
|
174
|
+
seq: 0, // auto-assigned by sink
|
|
175
|
+
session_id: sessionId ?? "",
|
|
176
|
+
source: "event-hook",
|
|
177
|
+
schema_version: SCHEMA_VERSION,
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
payload,
|
|
180
|
+
})
|
|
77
181
|
}
|
|
78
182
|
|
|
79
183
|
const hook: EventHookFn = async (input): Promise<void> => {
|
|
80
|
-
const
|
|
184
|
+
const type = input.event.type
|
|
185
|
+
const sessionId = extractSessionId(input.event)
|
|
186
|
+
const sessionKey = sessionId && sessionId.length > 0 ? sessionId : activeSessionId
|
|
187
|
+
let stateForSession = getAuditState(sessionKey)
|
|
81
188
|
let preDeleteState: AuditState | null = null
|
|
189
|
+
const preDeleteSink = getEventSink(sessionKey)
|
|
82
190
|
|
|
83
191
|
switch (type) {
|
|
84
192
|
case "session.created": {
|
|
85
193
|
const dir = projectDir ?? process.cwd()
|
|
86
194
|
const { state } = createAuditState(dir)
|
|
87
|
-
|
|
195
|
+
if (sessionId && sessionId.length > 0) {
|
|
196
|
+
setSessionState(sessionId, state)
|
|
197
|
+
activeSessionId = sessionId
|
|
198
|
+
} else {
|
|
199
|
+
fallbackAuditState = state
|
|
200
|
+
}
|
|
201
|
+
stateForSession = state
|
|
88
202
|
break
|
|
89
203
|
}
|
|
90
204
|
|
|
91
205
|
case "session.idle": {
|
|
92
|
-
if (
|
|
206
|
+
if (stateForSession) {
|
|
93
207
|
logger.debug(
|
|
94
|
-
`Session idle — phase: ${
|
|
208
|
+
`Session idle — phase: ${stateForSession.currentPhase}, findings: ${stateForSession.findings.length}`,
|
|
95
209
|
)
|
|
96
210
|
}
|
|
97
211
|
break
|
|
98
212
|
}
|
|
99
213
|
|
|
100
214
|
case "session.error": {
|
|
101
|
-
if (
|
|
215
|
+
if (stateForSession) {
|
|
102
216
|
logger.error(
|
|
103
217
|
`Session error — state snapshot: ${JSON.stringify({
|
|
104
|
-
sessionId:
|
|
105
|
-
phase:
|
|
106
|
-
findingsCount:
|
|
107
|
-
contractsReviewed:
|
|
218
|
+
sessionId: stateForSession.sessionId,
|
|
219
|
+
phase: stateForSession.currentPhase,
|
|
220
|
+
findingsCount: stateForSession.findings.length,
|
|
221
|
+
contractsReviewed: stateForSession.contractsReviewed,
|
|
108
222
|
})}`,
|
|
109
223
|
)
|
|
110
224
|
}
|
|
@@ -112,8 +226,7 @@ export function createEventHook(
|
|
|
112
226
|
}
|
|
113
227
|
|
|
114
228
|
case "session.deleted": {
|
|
115
|
-
preDeleteState =
|
|
116
|
-
currentAuditState = null
|
|
229
|
+
preDeleteState = stateForSession
|
|
117
230
|
break
|
|
118
231
|
}
|
|
119
232
|
|
|
@@ -123,11 +236,16 @@ export function createEventHook(
|
|
|
123
236
|
|
|
124
237
|
for (const handler of subHandlers) {
|
|
125
238
|
try {
|
|
239
|
+
const setStateForSession = (state: AuditState | null): void => {
|
|
240
|
+
setAuditState(state, sessionKey)
|
|
241
|
+
stateForSession = state
|
|
242
|
+
}
|
|
243
|
+
|
|
126
244
|
await handler({
|
|
127
245
|
type,
|
|
128
246
|
sessionId,
|
|
129
|
-
auditState:
|
|
130
|
-
setAuditState,
|
|
247
|
+
auditState: stateForSession,
|
|
248
|
+
setAuditState: setStateForSession,
|
|
131
249
|
})
|
|
132
250
|
} catch (error) {
|
|
133
251
|
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
@@ -135,24 +253,32 @@ export function createEventHook(
|
|
|
135
253
|
}
|
|
136
254
|
|
|
137
255
|
// Emit canonical events to sink (after sub-handlers, so sink may have been set during session.created)
|
|
256
|
+
const sinkForSession = getEventSink(sessionKey)
|
|
138
257
|
switch (type) {
|
|
139
258
|
case "session.created": {
|
|
140
|
-
if (
|
|
141
|
-
await emitToSink(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
259
|
+
if (stateForSession) {
|
|
260
|
+
await emitToSink(
|
|
261
|
+
sinkForSession,
|
|
262
|
+
"session.created",
|
|
263
|
+
stateForSession.sessionId,
|
|
264
|
+
sessionId,
|
|
265
|
+
{
|
|
266
|
+
projectDir: stateForSession.projectDir,
|
|
267
|
+
sessionId: stateForSession.sessionId,
|
|
268
|
+
plugin_version: ARGUS_PLUGIN_VERSION,
|
|
269
|
+
scope: stateForSession.scope,
|
|
270
|
+
},
|
|
271
|
+
)
|
|
146
272
|
}
|
|
147
273
|
break
|
|
148
274
|
}
|
|
149
275
|
|
|
150
276
|
case "session.idle": {
|
|
151
|
-
if (
|
|
152
|
-
await emitToSink("session.idle",
|
|
153
|
-
findingsCount:
|
|
154
|
-
toolsExecutedCount:
|
|
155
|
-
phase:
|
|
277
|
+
if (stateForSession) {
|
|
278
|
+
await emitToSink(sinkForSession, "session.idle", stateForSession.sessionId, sessionId, {
|
|
279
|
+
findingsCount: stateForSession.findings.length,
|
|
280
|
+
toolsExecutedCount: stateForSession.toolsExecuted.length,
|
|
281
|
+
phase: stateForSession.currentPhase,
|
|
156
282
|
})
|
|
157
283
|
}
|
|
158
284
|
break
|
|
@@ -160,17 +286,29 @@ export function createEventHook(
|
|
|
160
286
|
|
|
161
287
|
case "session.deleted": {
|
|
162
288
|
if (preDeleteState) {
|
|
163
|
-
await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
|
|
289
|
+
await emitToSink(preDeleteSink, "session.deleted", preDeleteState.sessionId, sessionId, {
|
|
164
290
|
archived: true,
|
|
165
291
|
plugin_version: ARGUS_PLUGIN_VERSION,
|
|
166
292
|
})
|
|
167
293
|
|
|
168
|
-
|
|
294
|
+
const hasSiblingSessionForRun =
|
|
295
|
+
typeof sessionKey === "string" && sessionKey.length > 0
|
|
296
|
+
? Array.from(sinksBySessionId.entries()).some(
|
|
297
|
+
([mappedSessionId, mappedSink]) =>
|
|
298
|
+
mappedSessionId !== sessionKey && mappedSink.runId === preDeleteState.sessionId,
|
|
299
|
+
)
|
|
300
|
+
: false
|
|
301
|
+
|
|
302
|
+
if (preDeleteSink && !preDeleteSink.isFinalized && !hasSiblingSessionForRun) {
|
|
169
303
|
try {
|
|
170
304
|
lastFinalizationResult = await finalizeRun(
|
|
171
305
|
preDeleteState.sessionId,
|
|
172
306
|
preDeleteState.projectDir,
|
|
173
|
-
|
|
307
|
+
preDeleteSink,
|
|
308
|
+
)
|
|
309
|
+
void updateRunStatus(
|
|
310
|
+
preDeleteState.sessionId,
|
|
311
|
+
lastFinalizationResult.invariantsPassed ? "finalized" : "failed",
|
|
174
312
|
)
|
|
175
313
|
} catch (error) {
|
|
176
314
|
logger.error(
|
|
@@ -179,7 +317,18 @@ export function createEventHook(
|
|
|
179
317
|
}
|
|
180
318
|
}
|
|
181
319
|
}
|
|
182
|
-
|
|
320
|
+
|
|
321
|
+
if (sessionKey && sessionKey.length > 0) {
|
|
322
|
+
statesBySessionId.delete(sessionKey)
|
|
323
|
+
sinksBySessionId.delete(sessionKey)
|
|
324
|
+
if (activeSessionId === sessionKey) {
|
|
325
|
+
const nextSession = statesBySessionId.keys().next().value
|
|
326
|
+
activeSessionId = typeof nextSession === "string" ? nextSession : ""
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
fallbackAuditState = null
|
|
330
|
+
fallbackEventSink = null
|
|
331
|
+
}
|
|
183
332
|
break
|
|
184
333
|
}
|
|
185
334
|
|
|
@@ -190,5 +339,12 @@ export function createEventHook(
|
|
|
190
339
|
|
|
191
340
|
const getLastFinalizationResult = (): FinalizationResult | null => lastFinalizationResult
|
|
192
341
|
|
|
193
|
-
return {
|
|
342
|
+
return {
|
|
343
|
+
hook,
|
|
344
|
+
getAuditState,
|
|
345
|
+
setAuditState,
|
|
346
|
+
setEventSink,
|
|
347
|
+
getEventSink,
|
|
348
|
+
getLastFinalizationResult,
|
|
349
|
+
}
|
|
194
350
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import os from "node:os"
|
|
2
|
-
import path from "node:path"
|
|
3
1
|
import type { ArgusConfig } from "../config/types"
|
|
4
2
|
import { ScvdClient } from "../knowledge/scvd-client"
|
|
5
3
|
import { type SyncResult, syncIncremental } from "../knowledge/scvd-sync"
|
|
4
|
+
import { getScvdIndexPath } from "../shared/cache-paths"
|
|
6
5
|
import { createLogger } from "../shared/logger"
|
|
7
6
|
|
|
8
7
|
export type KnowledgeSyncDependencies = {
|
|
@@ -36,7 +35,7 @@ export function createKnowledgeSyncHook(
|
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
|
|
39
|
-
const indexPath =
|
|
38
|
+
const indexPath = getScvdIndexPath()
|
|
40
39
|
|
|
41
40
|
Promise.resolve().then(async () => {
|
|
42
41
|
try {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { createLogger } from "../shared/logger"
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export interface SafeCreateHookOptions {
|
|
4
|
+
critical?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function safeCreateHook<T>(
|
|
8
|
+
factory: () => T,
|
|
9
|
+
hookName: string,
|
|
10
|
+
options: SafeCreateHookOptions = {},
|
|
11
|
+
): T | undefined {
|
|
12
|
+
const { critical = false } = options
|
|
4
13
|
try {
|
|
5
14
|
return factory()
|
|
6
15
|
} catch (error) {
|
|
@@ -8,6 +17,9 @@ export function safeCreateHook<T>(factory: () => T, hookName: string): T | undef
|
|
|
8
17
|
logger.error(
|
|
9
18
|
`Failed to create hook "${hookName}": ${error instanceof Error ? error.message : String(error)}`,
|
|
10
19
|
)
|
|
20
|
+
if (critical) {
|
|
21
|
+
throw error
|
|
22
|
+
}
|
|
11
23
|
return undefined
|
|
12
24
|
}
|
|
13
25
|
}
|
|
@@ -1,29 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { computeMissingKeyTools, KEY_TOOLS, TOOL_SHORT_NAMES } from "../shared/key-tools"
|
|
2
|
+
import { estimateTokens } from "../shared/token-utils"
|
|
3
|
+
import { countBySeverity } from "../shared/validation-constants"
|
|
4
|
+
import type { AuditState } from "../state/types"
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
const TOKENS_PER_CHAR = 4
|
|
5
|
-
|
|
6
|
-
const TOOL_SHORT_NAMES: Record<string, string> = {
|
|
7
|
-
argus_slither_analyze: "slither",
|
|
8
|
-
argus_forge_test: "forge-test",
|
|
9
|
-
argus_check_patterns: "patterns",
|
|
10
|
-
argus_solodit_search: "solodit",
|
|
11
|
-
argus_analyze_contract: "analyzer",
|
|
12
|
-
}
|
|
13
|
-
const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
|
|
6
|
+
export { estimateTokens }
|
|
14
7
|
|
|
15
|
-
|
|
16
|
-
const UNAVAILABLE_TO_KEY_TOOL: Record<string, string> = {
|
|
17
|
-
slither: "slither",
|
|
18
|
-
forge: "forge-test",
|
|
19
|
-
solodit: "solodit",
|
|
20
|
-
}
|
|
8
|
+
const DEFAULT_TOKEN_BUDGET = 2000
|
|
21
9
|
|
|
22
10
|
export interface SystemPromptHookDeps {
|
|
23
|
-
getAuditState: () => AuditState | null
|
|
11
|
+
getAuditState: (sessionId?: string) => AuditState | null
|
|
24
12
|
getAgentForSession: (sessionID: string) => string | undefined
|
|
25
13
|
isArgusAgent: (sessionID: string) => boolean
|
|
26
|
-
getContextPressure?: (systemText: string) => number
|
|
14
|
+
getContextPressure?: (systemText: string, sessionId?: string) => number
|
|
27
15
|
getTokenBudget?: (agent: string, contextPressure: number) => number
|
|
28
16
|
getEnforcerReminder?: (state: AuditState) => string | null
|
|
29
17
|
getReconBlock?: () => string | null
|
|
@@ -47,26 +35,12 @@ export function buildFallbackDirectives(unavailableTools: string[]): string[] {
|
|
|
47
35
|
return directives
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
export function estimateTokens(text: string): number {
|
|
51
|
-
return Math.ceil(text.length / TOKENS_PER_CHAR)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
38
|
export function buildDynamicContext(
|
|
55
39
|
auditState: AuditState,
|
|
56
40
|
agent: string,
|
|
57
41
|
tokenBudget: number = DEFAULT_TOKEN_BUDGET,
|
|
58
42
|
): string {
|
|
59
|
-
const severityCounts
|
|
60
|
-
Critical: 0,
|
|
61
|
-
High: 0,
|
|
62
|
-
Medium: 0,
|
|
63
|
-
Low: 0,
|
|
64
|
-
Informational: 0,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
for (const finding of auditState.findings) {
|
|
68
|
-
severityCounts[finding.severity]++
|
|
69
|
-
}
|
|
43
|
+
const severityCounts = countBySeverity(auditState.findings)
|
|
70
44
|
|
|
71
45
|
const executedToolNames = new Set(
|
|
72
46
|
auditState.toolsExecuted.map((t) => TOOL_SHORT_NAMES[t.tool] ?? t.tool),
|
|
@@ -76,14 +50,14 @@ export function buildDynamicContext(
|
|
|
76
50
|
(t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
|
|
77
51
|
).join(" ")
|
|
78
52
|
const unavailable = auditState.unavailableTools ?? []
|
|
79
|
-
const
|
|
80
|
-
const pendingKeyTools = KEY_TOOLS.filter((t) => !executedToolNames.has(t) && !excusedTools.has(t))
|
|
53
|
+
const pendingKeyTools = computeMissingKeyTools(auditState.toolsExecuted, unavailable)
|
|
81
54
|
const gateStatus =
|
|
82
55
|
pendingKeyTools.length > 0
|
|
83
56
|
? `REPORTING GATE: BLOCKED \u2014 key tools pending: ${pendingKeyTools.join(", ")}`
|
|
84
57
|
: "REPORTING GATE: ALLOWED"
|
|
85
58
|
const lines: string[] = [
|
|
86
59
|
`<argus-context agent="${agent}">`,
|
|
60
|
+
...(auditState.sessionId ? [`run_id: ${auditState.sessionId}`] : []),
|
|
87
61
|
gateStatus,
|
|
88
62
|
`Phase: ${auditState.currentPhase}`,
|
|
89
63
|
`Contracts: ${auditState.contractsReviewed.length} reviewed`,
|
|
@@ -97,6 +71,12 @@ export function buildDynamicContext(
|
|
|
97
71
|
lines.push(...buildFallbackDirectives(unavailable))
|
|
98
72
|
}
|
|
99
73
|
|
|
74
|
+
if (auditState.currentPhase === "reporting" && !auditState.reportGenerated) {
|
|
75
|
+
lines.push(
|
|
76
|
+
"REPORT GENERATION: INCOMPLETE — Scribe was dispatched but argus_generate_report was not called. Re-dispatch Scribe or call argus_generate_report directly.",
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
100
80
|
lines.push("</argus-context>")
|
|
101
81
|
|
|
102
82
|
let summary = lines.join("\n")
|
|
@@ -105,6 +85,7 @@ export function buildDynamicContext(
|
|
|
105
85
|
const doneCount = KEY_TOOLS.filter((t) => executedToolNames.has(t)).length
|
|
106
86
|
summary = [
|
|
107
87
|
`<argus-context agent="${agent}">`,
|
|
88
|
+
...(auditState.sessionId ? [`run_id: ${auditState.sessionId}`] : []),
|
|
108
89
|
gateStatus,
|
|
109
90
|
`Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length} | Tasks: ${doneCount}/${KEY_TOOLS.length} done`,
|
|
110
91
|
"</argus-context>",
|
|
@@ -127,7 +108,7 @@ export function createSystemPromptHook(deps: SystemPromptHookDeps) {
|
|
|
127
108
|
return
|
|
128
109
|
}
|
|
129
110
|
|
|
130
|
-
const auditState = deps.getAuditState()
|
|
111
|
+
const auditState = deps.getAuditState(input.sessionID)
|
|
131
112
|
if (!auditState) {
|
|
132
113
|
return
|
|
133
114
|
}
|
|
@@ -138,7 +119,7 @@ export function createSystemPromptHook(deps: SystemPromptHookDeps) {
|
|
|
138
119
|
}
|
|
139
120
|
|
|
140
121
|
const currentSystem = output.system.join("\n")
|
|
141
|
-
const pressure = deps.getContextPressure?.(currentSystem) ?? 0
|
|
122
|
+
const pressure = deps.getContextPressure?.(currentSystem, input.sessionID) ?? 0
|
|
142
123
|
const budget = deps.getTokenBudget?.(agent, pressure) ?? DEFAULT_TOKEN_BUDGET
|
|
143
124
|
|
|
144
125
|
output.system.push(buildDynamicContext(auditState, agent, budget))
|