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
|
@@ -2,13 +2,28 @@ import { mkdir, rename } from "node:fs/promises"
|
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
3
|
import type { AuditStateManager } from "../../managers/types"
|
|
4
4
|
import { createLogger } from "../../shared/logger"
|
|
5
|
+
import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
|
|
5
6
|
import { createAuditState } from "../../state/audit-state"
|
|
7
|
+
import { projectAuditState, stableHash } from "../../state/projectors"
|
|
6
8
|
import type { AuditState, PersistentAuditState } from "../../state/types"
|
|
9
|
+
import { readEvents } from "./event-sink"
|
|
7
10
|
|
|
8
|
-
const STATE_FILE_DIR = ".opencode"
|
|
9
11
|
const STATE_FILE_NAME = "argus-state.json"
|
|
10
12
|
const STATE_VERSION = "2"
|
|
11
13
|
|
|
14
|
+
type ProjectedAuditCore = Pick<
|
|
15
|
+
AuditState,
|
|
16
|
+
"contractsReviewed" | "findings" | "toolsExecuted" | "currentPhase" | "scope"
|
|
17
|
+
>
|
|
18
|
+
|
|
19
|
+
interface ConsistentStateResult {
|
|
20
|
+
state: AuditState
|
|
21
|
+
sourceOfTruth: "events" | "snapshot"
|
|
22
|
+
lastEventSeq?: number
|
|
23
|
+
eventStreamHash?: string
|
|
24
|
+
repaired: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
13
28
|
return typeof value === "object" && value !== null
|
|
14
29
|
}
|
|
@@ -46,6 +61,54 @@ function isPersistentAuditState(value: unknown): value is PersistentAuditState {
|
|
|
46
61
|
)
|
|
47
62
|
}
|
|
48
63
|
|
|
64
|
+
function projectCoreState(
|
|
65
|
+
state: AuditState,
|
|
66
|
+
events: Awaited<ReturnType<typeof readEvents>>,
|
|
67
|
+
): ProjectedAuditCore {
|
|
68
|
+
const projected = projectAuditState(events, state.projectDir)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
contractsReviewed: projected.contractsReviewed,
|
|
72
|
+
findings: projected.findings,
|
|
73
|
+
toolsExecuted: projected.toolsExecuted,
|
|
74
|
+
currentPhase: projected.currentPhase,
|
|
75
|
+
scope: projected.scope,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasProjectedCoreMismatch(state: AuditState, projectedCore: ProjectedAuditCore): boolean {
|
|
80
|
+
const stateCore: ProjectedAuditCore = {
|
|
81
|
+
contractsReviewed: state.contractsReviewed,
|
|
82
|
+
findings: state.findings,
|
|
83
|
+
toolsExecuted: state.toolsExecuted,
|
|
84
|
+
currentPhase: state.currentPhase,
|
|
85
|
+
scope: state.scope,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return stableHash(stateCore) !== stableHash(projectedCore)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasSnapshotStampMismatch(
|
|
92
|
+
snapshotSeq: number | undefined,
|
|
93
|
+
snapshotHash: string | undefined,
|
|
94
|
+
derivedSeq: number | undefined,
|
|
95
|
+
derivedHash: string | undefined,
|
|
96
|
+
): boolean {
|
|
97
|
+
if (snapshotSeq === undefined && snapshotHash === undefined) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (snapshotSeq !== undefined && derivedSeq !== undefined && snapshotSeq !== derivedSeq) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (snapshotHash !== undefined && derivedHash !== undefined && snapshotHash !== derivedHash) {
|
|
106
|
+
return true
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
49
112
|
export function createDebouncedSave(
|
|
50
113
|
saveState: (state: AuditState) => Promise<void>,
|
|
51
114
|
delayMs = 5_000,
|
|
@@ -95,14 +158,73 @@ export function createDebouncedSave(
|
|
|
95
158
|
}
|
|
96
159
|
}
|
|
97
160
|
|
|
98
|
-
export function createAuditStateManager(
|
|
161
|
+
export function createAuditStateManager(
|
|
162
|
+
projectDir: string,
|
|
163
|
+
resolver: ArgusRootResolver = defaultRootResolver,
|
|
164
|
+
): AuditStateManager {
|
|
99
165
|
const logger = createLogger()
|
|
100
|
-
|
|
166
|
+
|
|
167
|
+
const stateFilePath = join(resolver.writeRoot(projectDir), STATE_FILE_NAME)
|
|
101
168
|
let currentState: AuditState = createAuditState(projectDir).state
|
|
102
169
|
|
|
170
|
+
async function deriveConsistentState(state: AuditState): Promise<ConsistentStateResult> {
|
|
171
|
+
if (!state.sessionId || !state.projectDir) {
|
|
172
|
+
return {
|
|
173
|
+
state,
|
|
174
|
+
sourceOfTruth: "snapshot",
|
|
175
|
+
repaired: false,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const events = await readEvents(state.sessionId, state.projectDir, resolver)
|
|
181
|
+
const lastEventSeq = events.at(-1)?.seq ?? 0
|
|
182
|
+
const eventStreamHash = stableHash(events)
|
|
183
|
+
|
|
184
|
+
if (events.length === 0) {
|
|
185
|
+
return {
|
|
186
|
+
state,
|
|
187
|
+
sourceOfTruth: "events",
|
|
188
|
+
lastEventSeq,
|
|
189
|
+
eventStreamHash,
|
|
190
|
+
repaired: false,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const projectedCore = projectCoreState(state, events)
|
|
195
|
+
const repaired = hasProjectedCoreMismatch(state, projectedCore)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
state: repaired
|
|
199
|
+
? {
|
|
200
|
+
...state,
|
|
201
|
+
...projectedCore,
|
|
202
|
+
}
|
|
203
|
+
: state,
|
|
204
|
+
sourceOfTruth: "events",
|
|
205
|
+
lastEventSeq,
|
|
206
|
+
eventStreamHash,
|
|
207
|
+
repaired,
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
logger.warn(
|
|
211
|
+
`Failed to derive state from events for run ${state.sessionId}; using snapshot fallback`,
|
|
212
|
+
error,
|
|
213
|
+
)
|
|
214
|
+
return {
|
|
215
|
+
state,
|
|
216
|
+
sourceOfTruth: "snapshot",
|
|
217
|
+
repaired: false,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
103
222
|
async function load(): Promise<AuditState | null> {
|
|
104
223
|
try {
|
|
105
|
-
const
|
|
224
|
+
const resolvedPath = resolver.resolveReadPath(projectDir, STATE_FILE_NAME)
|
|
225
|
+
const readPath = resolvedPath ?? stateFilePath
|
|
226
|
+
|
|
227
|
+
const file = Bun.file(readPath)
|
|
106
228
|
if (!(await file.exists())) {
|
|
107
229
|
return null
|
|
108
230
|
}
|
|
@@ -114,11 +236,19 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
114
236
|
|
|
115
237
|
const parsed: unknown = JSON.parse(content)
|
|
116
238
|
if (!isPersistentAuditState(parsed)) {
|
|
117
|
-
logger.warn("Persistent audit state is invalid, ignoring",
|
|
239
|
+
logger.warn("Persistent audit state is invalid, ignoring", readPath)
|
|
118
240
|
return null
|
|
119
241
|
}
|
|
120
242
|
|
|
121
|
-
const {
|
|
243
|
+
const {
|
|
244
|
+
savedAt: _savedAt,
|
|
245
|
+
version,
|
|
246
|
+
filePath: _filePath,
|
|
247
|
+
source_of_truth: snapshotSourceOfTruth,
|
|
248
|
+
last_event_seq: snapshotSeq,
|
|
249
|
+
event_stream_hash: snapshotEventHash,
|
|
250
|
+
...state
|
|
251
|
+
} = parsed
|
|
122
252
|
|
|
123
253
|
if (version === "1") {
|
|
124
254
|
if (!state.soloditResults) {
|
|
@@ -129,7 +259,32 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
129
259
|
}
|
|
130
260
|
}
|
|
131
261
|
|
|
132
|
-
|
|
262
|
+
if (snapshotSeq !== undefined) {
|
|
263
|
+
logger.debug(`Loaded snapshot with last_event_seq=${snapshotSeq} from ${readPath}`)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const consistent = await deriveConsistentState(state)
|
|
267
|
+
const stampMismatch =
|
|
268
|
+
consistent.sourceOfTruth === "events" &&
|
|
269
|
+
hasSnapshotStampMismatch(
|
|
270
|
+
snapshotSeq,
|
|
271
|
+
snapshotEventHash,
|
|
272
|
+
consistent.lastEventSeq,
|
|
273
|
+
consistent.eventStreamHash,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
if (consistent.repaired || stampMismatch) {
|
|
277
|
+
const mismatchReason = consistent.repaired ? "projected core mismatch" : "stamp mismatch"
|
|
278
|
+
logger.warn(
|
|
279
|
+
`Recovered audit state from event stream for run ${state.sessionId}: ${mismatchReason}`,
|
|
280
|
+
)
|
|
281
|
+
} else if (snapshotSourceOfTruth === "events" && consistent.sourceOfTruth !== "events") {
|
|
282
|
+
logger.warn(
|
|
283
|
+
`Snapshot for run ${state.sessionId} was marked event-derived but could not be validated against events`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
currentState = consistent.state
|
|
133
288
|
return currentState
|
|
134
289
|
} catch (err) {
|
|
135
290
|
logger.warn("Failed to load persisted audit state", err)
|
|
@@ -148,12 +303,23 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
148
303
|
try {
|
|
149
304
|
while (true) {
|
|
150
305
|
const stateToSave = currentState
|
|
306
|
+
const consistent = await deriveConsistentState(stateToSave)
|
|
307
|
+
|
|
308
|
+
if (consistent.repaired) {
|
|
309
|
+
logger.warn(
|
|
310
|
+
`State/core divergence detected for run ${stateToSave.sessionId}; auto-repairing`,
|
|
311
|
+
)
|
|
312
|
+
currentState = consistent.state
|
|
313
|
+
}
|
|
151
314
|
|
|
152
315
|
const persistentState: PersistentAuditState = {
|
|
153
|
-
...
|
|
316
|
+
...consistent.state,
|
|
154
317
|
savedAt: Date.now(),
|
|
155
318
|
version: STATE_VERSION,
|
|
156
319
|
filePath: stateFilePath,
|
|
320
|
+
source_of_truth: consistent.sourceOfTruth,
|
|
321
|
+
last_event_seq: consistent.lastEventSeq,
|
|
322
|
+
event_stream_hash: consistent.eventStreamHash,
|
|
157
323
|
}
|
|
158
324
|
|
|
159
325
|
const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`
|
|
@@ -161,7 +327,7 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
161
327
|
await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`)
|
|
162
328
|
await rename(tempFilePath, stateFilePath)
|
|
163
329
|
|
|
164
|
-
if (currentState ===
|
|
330
|
+
if (currentState === consistent.state) break
|
|
165
331
|
}
|
|
166
332
|
} catch (err) {
|
|
167
333
|
logger.warn("Failed to persist audit state", err)
|
|
@@ -197,14 +363,18 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
197
363
|
|
|
198
364
|
if (hasContent) {
|
|
199
365
|
try {
|
|
366
|
+
const consistent = await deriveConsistentState(currentState)
|
|
200
367
|
const archivesDir = join(dirname(stateFilePath), "archives")
|
|
201
368
|
await mkdir(archivesDir, { recursive: true })
|
|
202
369
|
const archivePath = join(archivesDir, `argus-state.${Date.now()}.json`)
|
|
203
370
|
const persistentState: PersistentAuditState = {
|
|
204
|
-
...
|
|
371
|
+
...consistent.state,
|
|
205
372
|
savedAt: Date.now(),
|
|
206
373
|
version: STATE_VERSION,
|
|
207
374
|
filePath: archivePath,
|
|
375
|
+
source_of_truth: consistent.sourceOfTruth,
|
|
376
|
+
last_event_seq: consistent.lastEventSeq,
|
|
377
|
+
event_stream_hash: consistent.eventStreamHash,
|
|
208
378
|
}
|
|
209
379
|
await Bun.write(archivePath, `${JSON.stringify(persistentState, null, 2)}\n`)
|
|
210
380
|
} catch {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, rename } from "node:fs/promises"
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
|
+
import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
|
|
3
4
|
import type { AuditEvent, AuditEventType } from "../../state/schemas"
|
|
4
5
|
|
|
5
6
|
export type EventSinkErrorCode = "SEQUENCE_CONFLICT" | "INVALID_EVENT" | "IO_ERROR"
|
|
@@ -52,8 +53,8 @@ function createMutex() {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
function buildJournalPath(runId: string, projectDir: string): string {
|
|
56
|
-
return join(projectDir, "
|
|
56
|
+
function buildJournalPath(runId: string, projectDir: string, resolver: ArgusRootResolver): string {
|
|
57
|
+
return join(resolver.writeRoot(projectDir), "runs", runId, "events.jsonl")
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
async function readRawContent(path: string): Promise<string> {
|
|
@@ -85,8 +86,12 @@ function parseJournalLines(content: string): AuditEvent[] {
|
|
|
85
86
|
/**
|
|
86
87
|
* Replay-safe stateless read — returns all events for a run sorted by seq.
|
|
87
88
|
*/
|
|
88
|
-
export async function readEvents(
|
|
89
|
-
|
|
89
|
+
export async function readEvents(
|
|
90
|
+
runId: string,
|
|
91
|
+
projectDir: string,
|
|
92
|
+
resolver: ArgusRootResolver = defaultRootResolver,
|
|
93
|
+
): Promise<AuditEvent[]> {
|
|
94
|
+
const journalPath = buildJournalPath(runId, projectDir, resolver)
|
|
90
95
|
const content = await readRawContent(journalPath)
|
|
91
96
|
return parseJournalLines(content)
|
|
92
97
|
}
|
|
@@ -95,8 +100,12 @@ export async function readEvents(runId: string, projectDir: string): Promise<Aud
|
|
|
95
100
|
* Append-only event sink with monotonic seq allocation, in-process mutex,
|
|
96
101
|
* and atomic temp-file-then-rename writes. Restart-safe via journal replay.
|
|
97
102
|
*/
|
|
98
|
-
export function createEventSink(
|
|
99
|
-
|
|
103
|
+
export function createEventSink(
|
|
104
|
+
runId: string,
|
|
105
|
+
projectDir: string,
|
|
106
|
+
resolver: ArgusRootResolver = defaultRootResolver,
|
|
107
|
+
): EventSink {
|
|
108
|
+
const journalPath = buildJournalPath(runId, projectDir, resolver)
|
|
100
109
|
const mutex = createMutex()
|
|
101
110
|
let lastSeq = 0
|
|
102
111
|
let initialized = false
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
import { createAuditArtifactResolver } from "../../shared/audit-artifact-resolver"
|
|
4
|
+
import { dedupeFindingsForFinalOutput } from "../../state/finding-aggregation"
|
|
5
|
+
import { projectFindings, projectToolExecutions, stableHash } from "../../state/projectors"
|
|
6
|
+
import type { CanonicalFinding, CanonicalToolExecution } from "../../state/schemas"
|
|
7
|
+
import { SCHEMA_VERSION } from "../../state/schemas"
|
|
8
|
+
import { readEvents } from "./event-sink"
|
|
9
|
+
|
|
10
|
+
export interface FindingsArtifact {
|
|
11
|
+
run_id: string
|
|
12
|
+
session_id: string
|
|
13
|
+
schema_version: string
|
|
14
|
+
seq_first: number
|
|
15
|
+
seq_last: number
|
|
16
|
+
event_count: number
|
|
17
|
+
content_hash: string
|
|
18
|
+
generated_at: number
|
|
19
|
+
findings: CanonicalFinding[]
|
|
20
|
+
toolsExecuted: CanonicalToolExecution[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function materializeFindings(
|
|
24
|
+
runId: string,
|
|
25
|
+
projectDir: string,
|
|
26
|
+
sessionId?: string,
|
|
27
|
+
): Promise<FindingsArtifact> {
|
|
28
|
+
const events = await readEvents(runId, projectDir)
|
|
29
|
+
const findings = dedupeFindingsForFinalOutput(projectFindings(events))
|
|
30
|
+
const toolsExecuted = projectToolExecutions(events)
|
|
31
|
+
const contentHash = stableHash(JSON.stringify(findings))
|
|
32
|
+
const generatedAt = events.at(-1)?.timestamp ?? 0
|
|
33
|
+
|
|
34
|
+
const artifact: FindingsArtifact = {
|
|
35
|
+
run_id: runId,
|
|
36
|
+
session_id: sessionId ?? events[0]?.session_id ?? "",
|
|
37
|
+
schema_version: SCHEMA_VERSION,
|
|
38
|
+
seq_first: events[0]?.seq ?? 0,
|
|
39
|
+
seq_last: events.at(-1)?.seq ?? 0,
|
|
40
|
+
event_count: events.length,
|
|
41
|
+
content_hash: contentHash,
|
|
42
|
+
generated_at: generatedAt,
|
|
43
|
+
findings,
|
|
44
|
+
toolsExecuted,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const findingsFile = createAuditArtifactResolver(runId, projectDir).paths().findingsFile
|
|
48
|
+
await mkdir(dirname(findingsFile), { recursive: true })
|
|
49
|
+
await writeFile(findingsFile, JSON.stringify(artifact, null, 2))
|
|
50
|
+
|
|
51
|
+
return artifact
|
|
52
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createAuditStateManager } from "./audit-state-manager"
|
|
2
|
-
export { createEventSink, readEvents, EventSinkError } from "./event-sink"
|
|
3
2
|
export type { EventSink, EventSinkErrorCode } from "./event-sink"
|
|
3
|
+
export { createEventSink, EventSinkError, readEvents } from "./event-sink"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ARGUS_PLUGIN_VERSION } from "../../shared/plugin-metadata"
|
|
1
2
|
import { validateEventSequence } from "../../state/projectors"
|
|
2
3
|
import type { AuditEvent } from "../../state/schemas"
|
|
3
4
|
import { SCHEMA_VERSION } from "../../state/schemas"
|
|
@@ -12,18 +13,23 @@ export type FinalizationResult = {
|
|
|
12
13
|
timestamp: number
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function hasSessionCreated(events: AuditEvent[]): boolean {
|
|
16
|
+
export function hasSessionCreated(events: AuditEvent[]): boolean {
|
|
16
17
|
return events.some((event) => event.type === "session.created")
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function hasSessionDeleted(events: AuditEvent[]): boolean {
|
|
20
|
+
export function hasSessionDeleted(events: AuditEvent[]): boolean {
|
|
20
21
|
return events.some((event) => event.type === "session.deleted")
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
export type ToolLifecycleCheckResult = {
|
|
25
|
+
orphanedToolCallIds: string[]
|
|
26
|
+
malformedEvents: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function collectToolLifecycleIssues(events: AuditEvent[]): ToolLifecycleCheckResult {
|
|
24
30
|
const startedCallIds = new Set<string>()
|
|
25
31
|
const completedCallIds = new Set<string>()
|
|
26
|
-
const
|
|
32
|
+
const malformedEvents: string[] = []
|
|
27
33
|
|
|
28
34
|
for (const event of events) {
|
|
29
35
|
if (event.type !== "tool.started" && event.type !== "tool.completed") {
|
|
@@ -31,7 +37,7 @@ function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
if (typeof event.tool_call_id !== "string" || event.tool_call_id.length === 0) {
|
|
34
|
-
|
|
40
|
+
malformedEvents.push(`${event.type} at seq ${event.seq} missing tool_call_id`)
|
|
35
41
|
continue
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -44,13 +50,25 @@ function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
|
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
const orphanedToolCallIds: string[] = []
|
|
47
54
|
for (const toolCallId of startedCallIds) {
|
|
48
55
|
if (!completedCallIds.has(toolCallId)) {
|
|
49
|
-
|
|
56
|
+
orphanedToolCallIds.push(toolCallId)
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
return
|
|
60
|
+
return {
|
|
61
|
+
orphanedToolCallIds,
|
|
62
|
+
malformedEvents,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function collectOrphanedToolStarts(events: AuditEvent[]): string[] {
|
|
67
|
+
const { orphanedToolCallIds, malformedEvents } = collectToolLifecycleIssues(events)
|
|
68
|
+
const orphanedErrors = orphanedToolCallIds.map(
|
|
69
|
+
(toolCallId) => `orphaned tool.started without matching tool.completed: ${toolCallId}`,
|
|
70
|
+
)
|
|
71
|
+
return [...malformedEvents, ...orphanedErrors]
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -161,6 +179,7 @@ export async function finalizeRun(
|
|
|
161
179
|
invariantsPassed,
|
|
162
180
|
errors,
|
|
163
181
|
status: invariantsPassed ? "finalized" : "failed-finalization",
|
|
182
|
+
plugin_version: ARGUS_PLUGIN_VERSION,
|
|
164
183
|
},
|
|
165
184
|
})
|
|
166
185
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises"
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
3
|
import { createLogger } from "../../shared/logger"
|
|
4
|
+
import { type ArgusRootResolver, defaultRootResolver } from "../../shared/path-root-resolver"
|
|
4
5
|
|
|
5
6
|
const logger = createLogger()
|
|
6
7
|
|
|
7
|
-
const JOURNAL_DIR = ".opencode"
|
|
8
8
|
const JOURNAL_FILE = "argus-journal.jsonl"
|
|
9
9
|
|
|
10
10
|
export type JournalEvent =
|
|
@@ -15,7 +15,12 @@ export type JournalEvent =
|
|
|
15
15
|
findingsCount: number
|
|
16
16
|
toolsExecutedCount: number
|
|
17
17
|
}
|
|
18
|
-
| {
|
|
18
|
+
| {
|
|
19
|
+
type: "session.deleted"
|
|
20
|
+
timestamp: number
|
|
21
|
+
archived: boolean
|
|
22
|
+
finalizationPassed: boolean | null
|
|
23
|
+
}
|
|
19
24
|
| {
|
|
20
25
|
type: "tool.executed"
|
|
21
26
|
tool: string
|
|
@@ -30,12 +35,15 @@ export type JournalEvent =
|
|
|
30
35
|
findingsCount: number
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
export function createRunJournal(
|
|
38
|
+
export function createRunJournal(
|
|
39
|
+
projectDir: string,
|
|
40
|
+
resolver: ArgusRootResolver = defaultRootResolver,
|
|
41
|
+
): {
|
|
34
42
|
log(event: JournalEvent): void
|
|
35
43
|
close(): Promise<void>
|
|
36
44
|
getPath(): string
|
|
37
45
|
} {
|
|
38
|
-
const journalPath = join(projectDir,
|
|
46
|
+
const journalPath = join(resolver.writeRoot(projectDir), JOURNAL_FILE)
|
|
39
47
|
let ensureDirPromise: Promise<void> | null = null
|
|
40
48
|
const pendingWrites = new Set<Promise<void>>()
|
|
41
49
|
|
package/src/hooks/event-hook.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EventSink } from "../features/persistent-state/event-sink"
|
|
2
|
-
import { finalizeRun } from "../features/persistent-state/run-finalizer"
|
|
3
2
|
import type { FinalizationResult } from "../features/persistent-state/run-finalizer"
|
|
3
|
+
import { finalizeRun } from "../features/persistent-state/run-finalizer"
|
|
4
4
|
import { createLogger } from "../shared/logger"
|
|
5
|
+
import { ARGUS_PLUGIN_VERSION } from "../shared/plugin-metadata"
|
|
5
6
|
import { createAuditState } from "../state/audit-state"
|
|
6
7
|
import type { AuditEvent } from "../state/schemas"
|
|
7
8
|
import { SCHEMA_VERSION } from "../state/schemas"
|
|
@@ -140,6 +141,7 @@ export function createEventHook(
|
|
|
140
141
|
await emitToSink("session.created", currentAuditState.sessionId, sessionId, {
|
|
141
142
|
projectDir: currentAuditState.projectDir,
|
|
142
143
|
sessionId: currentAuditState.sessionId,
|
|
144
|
+
plugin_version: ARGUS_PLUGIN_VERSION,
|
|
143
145
|
})
|
|
144
146
|
}
|
|
145
147
|
break
|
|
@@ -160,6 +162,7 @@ export function createEventHook(
|
|
|
160
162
|
if (preDeleteState) {
|
|
161
163
|
await emitToSink("session.deleted", preDeleteState.sessionId, sessionId, {
|
|
162
164
|
archived: true,
|
|
165
|
+
plugin_version: ARGUS_PLUGIN_VERSION,
|
|
163
166
|
})
|
|
164
167
|
|
|
165
168
|
if (eventSink) {
|
|
@@ -12,6 +12,13 @@ const TOOL_SHORT_NAMES: Record<string, string> = {
|
|
|
12
12
|
}
|
|
13
13
|
const KEY_TOOLS = ["slither", "forge-test", "patterns", "solodit", "analyzer"]
|
|
14
14
|
|
|
15
|
+
/** Maps unavailable-tool short names to their KEY_TOOLS counterpart */
|
|
16
|
+
const UNAVAILABLE_TO_KEY_TOOL: Record<string, string> = {
|
|
17
|
+
slither: "slither",
|
|
18
|
+
forge: "forge-test",
|
|
19
|
+
solodit: "solodit",
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
export interface SystemPromptHookDeps {
|
|
16
23
|
getAuditState: () => AuditState | null
|
|
17
24
|
getAgentForSession: (sessionID: string) => string | undefined
|
|
@@ -69,8 +76,15 @@ export function buildDynamicContext(
|
|
|
69
76
|
(t) => `${t}=${executedToolNames.has(t) ? "done" : "pending"}`,
|
|
70
77
|
).join(" ")
|
|
71
78
|
const unavailable = auditState.unavailableTools ?? []
|
|
79
|
+
const excusedTools = new Set(unavailable.map((t) => UNAVAILABLE_TO_KEY_TOOL[t]).filter(Boolean))
|
|
80
|
+
const pendingKeyTools = KEY_TOOLS.filter((t) => !executedToolNames.has(t) && !excusedTools.has(t))
|
|
81
|
+
const gateStatus =
|
|
82
|
+
pendingKeyTools.length > 0
|
|
83
|
+
? `REPORTING GATE: BLOCKED \u2014 key tools pending: ${pendingKeyTools.join(", ")}`
|
|
84
|
+
: "REPORTING GATE: ALLOWED"
|
|
72
85
|
const lines: string[] = [
|
|
73
86
|
`<argus-context agent="${agent}">`,
|
|
87
|
+
gateStatus,
|
|
74
88
|
`Phase: ${auditState.currentPhase}`,
|
|
75
89
|
`Contracts: ${auditState.contractsReviewed.length} reviewed`,
|
|
76
90
|
`Findings: Critical=${severityCounts.Critical} High=${severityCounts.High} Medium=${severityCounts.Medium} Low=${severityCounts.Low} Info=${severityCounts.Informational}`,
|
|
@@ -91,6 +105,7 @@ export function buildDynamicContext(
|
|
|
91
105
|
const doneCount = KEY_TOOLS.filter((t) => executedToolNames.has(t)).length
|
|
92
106
|
summary = [
|
|
93
107
|
`<argus-context agent="${agent}">`,
|
|
108
|
+
gateStatus,
|
|
94
109
|
`Phase: ${auditState.currentPhase} | Findings: ${auditState.findings.length} | Contracts: ${auditState.contractsReviewed.length} | Tasks: ${doneCount}/${KEY_TOOLS.length} done`,
|
|
95
110
|
"</argus-context>",
|
|
96
111
|
].join("\n")
|