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
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,7 +226,7 @@ export function createEventHook(
|
|
|
112
226
|
}
|
|
113
227
|
|
|
114
228
|
case "session.deleted": {
|
|
115
|
-
preDeleteState =
|
|
229
|
+
preDeleteState = stateForSession
|
|
116
230
|
break
|
|
117
231
|
}
|
|
118
232
|
|
|
@@ -122,11 +236,16 @@ export function createEventHook(
|
|
|
122
236
|
|
|
123
237
|
for (const handler of subHandlers) {
|
|
124
238
|
try {
|
|
239
|
+
const setStateForSession = (state: AuditState | null): void => {
|
|
240
|
+
setAuditState(state, sessionKey)
|
|
241
|
+
stateForSession = state
|
|
242
|
+
}
|
|
243
|
+
|
|
125
244
|
await handler({
|
|
126
245
|
type,
|
|
127
246
|
sessionId,
|
|
128
|
-
auditState:
|
|
129
|
-
setAuditState,
|
|
247
|
+
auditState: stateForSession,
|
|
248
|
+
setAuditState: setStateForSession,
|
|
130
249
|
})
|
|
131
250
|
} catch (error) {
|
|
132
251
|
logger.error(`Sub-handler failed for event ${type}:`, error)
|
|
@@ -134,24 +253,32 @@ export function createEventHook(
|
|
|
134
253
|
}
|
|
135
254
|
|
|
136
255
|
// Emit canonical events to sink (after sub-handlers, so sink may have been set during session.created)
|
|
256
|
+
const sinkForSession = getEventSink(sessionKey)
|
|
137
257
|
switch (type) {
|
|
138
258
|
case "session.created": {
|
|
139
|
-
if (
|
|
140
|
-
await emitToSink(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
)
|
|
145
272
|
}
|
|
146
273
|
break
|
|
147
274
|
}
|
|
148
275
|
|
|
149
276
|
case "session.idle": {
|
|
150
|
-
if (
|
|
151
|
-
await emitToSink("session.idle",
|
|
152
|
-
findingsCount:
|
|
153
|
-
toolsExecutedCount:
|
|
154
|
-
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,
|
|
155
282
|
})
|
|
156
283
|
}
|
|
157
284
|
break
|
|
@@ -159,17 +286,29 @@ export function createEventHook(
|
|
|
159
286
|
|
|
160
287
|
case "session.deleted": {
|
|
161
288
|
if (preDeleteState) {
|
|
162
|
-
await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
|
|
289
|
+
await emitToSink(preDeleteSink, "session.deleted", preDeleteState.sessionId, sessionId, {
|
|
163
290
|
archived: true,
|
|
164
291
|
plugin_version: ARGUS_PLUGIN_VERSION,
|
|
165
292
|
})
|
|
166
293
|
|
|
167
|
-
|
|
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) {
|
|
168
303
|
try {
|
|
169
304
|
lastFinalizationResult = await finalizeRun(
|
|
170
305
|
preDeleteState.sessionId,
|
|
171
306
|
preDeleteState.projectDir,
|
|
172
|
-
|
|
307
|
+
preDeleteSink,
|
|
308
|
+
)
|
|
309
|
+
void updateRunStatus(
|
|
310
|
+
preDeleteState.sessionId,
|
|
311
|
+
lastFinalizationResult.invariantsPassed ? "finalized" : "failed",
|
|
173
312
|
)
|
|
174
313
|
} catch (error) {
|
|
175
314
|
logger.error(
|
|
@@ -178,8 +317,18 @@ export function createEventHook(
|
|
|
178
317
|
}
|
|
179
318
|
}
|
|
180
319
|
}
|
|
181
|
-
|
|
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))
|