solidity-argus 0.1.8 → 0.2.0
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/README.md +161 -1
- package/package.json +5 -2
- package/skills/README.md +63 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +11 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +11 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/patterns/access-control.yaml +31 -0
- package/skills/patterns/erc4626.yaml +29 -0
- package/skills/patterns/flash-loan.yaml +20 -0
- package/skills/patterns/oracle.yaml +30 -0
- package/skills/patterns/proxy.yaml +30 -0
- package/skills/patterns/reentrancy.yaml +30 -0
- package/skills/patterns/signature.yaml +31 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
- package/src/agents/argus-prompt.ts +4 -4
- package/src/agents/pythia-prompt.ts +4 -4
- package/src/agents/scribe-prompt.ts +3 -3
- package/src/agents/sentinel-prompt.ts +4 -4
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +9 -5
- package/src/cli/commands/doctor.ts +274 -16
- package/src/cli/commands/init.ts +5 -5
- package/src/cli/commands/install.ts +5 -5
- package/src/cli/commands/lint-skills.ts +114 -0
- package/src/cli/tui-prompts.ts +4 -2
- package/src/config/schema.ts +2 -0
- package/src/create-hooks.ts +99 -14
- package/src/create-tools.ts +2 -0
- package/src/features/error-recovery/tool-error-recovery.ts +74 -19
- package/src/features/persistent-state/audit-state-manager.ts +36 -13
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +3 -0
- package/src/hooks/context-budget.ts +45 -0
- package/src/hooks/event-hook.ts +5 -4
- package/src/hooks/knowledge-sync-hook.ts +2 -1
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +4 -5
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +86 -7
- package/src/index.ts +24 -1
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +37 -10
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +53 -3
- package/src/knowledge/scvd-sync.ts +205 -34
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/plugin-interface.ts +14 -1
- package/src/shared/binary-utils.ts +1 -0
- package/src/shared/logger.ts +78 -17
- package/src/skills/argus-skill-resolver.ts +226 -0
- package/src/skills/skill-schema.ts +98 -0
- package/src/state/audit-state.ts +2 -0
- package/src/state/types.ts +32 -1
- package/src/tools/argus-skill-load-tool.ts +73 -0
- package/src/tools/pattern-checker-tool.ts +56 -12
- package/src/tools/pattern-loader.ts +183 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/report-generator-tool.ts +134 -11
- package/src/tools/slither-tool.ts +61 -19
- package/src/tools/solodit-search-tool.ts +92 -14
- package/src/utils/audit-artifact-detector.ts +119 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +128 -26
- package/src/utils/solidity-parser.ts +20 -4
- package/src/utils/solodit-health.ts +29 -0
package/src/create-hooks.ts
CHANGED
|
@@ -6,20 +6,53 @@ import { createConfigHandler } from "./hooks/config-handler"
|
|
|
6
6
|
import { createCompactionHook } from "./hooks/compaction-hook"
|
|
7
7
|
import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
|
|
8
8
|
import { createEventHookV2 } from "./hooks/event-hook-v2"
|
|
9
|
+
import { createAgentTracker } from "./hooks/agent-tracker"
|
|
10
|
+
import { createSystemPromptHook } from "./hooks/system-prompt-hook"
|
|
9
11
|
import { safeCreateHook } from "./hooks/safe-create-hook"
|
|
10
|
-
import { createToolOutputTruncator } from "./features/context-monitor"
|
|
12
|
+
import { createContextMonitor, createToolOutputTruncator } from "./features/context-monitor"
|
|
13
|
+
import { createAuditEnforcer } from "./features/audit-enforcer/audit-enforcer"
|
|
14
|
+
import { getTokenBudgetForAgent } from "./hooks/context-budget"
|
|
11
15
|
import { createSessionRecoveryHandler } from "./features/error-recovery"
|
|
12
16
|
import { createToolErrorRecoveryHandler } from "./features/error-recovery"
|
|
17
|
+
import { detectProject } from "./utils/project-detector"
|
|
18
|
+
import type { ProjectConfig } from "./utils/project-detector"
|
|
19
|
+
import { detectAuditArtifacts } from "./utils/audit-artifact-detector"
|
|
20
|
+
import type { ReconContext } from "./hooks/recon-context-builder"
|
|
21
|
+
import { buildReconContextBlock } from "./hooks/recon-context-builder"
|
|
22
|
+
import type { AuditState } from "./state/types"
|
|
23
|
+
|
|
24
|
+
let latestAgentTracker: ReturnType<typeof createAgentTracker> | undefined
|
|
25
|
+
|
|
26
|
+
export function getAgentForSession(sessionID: string): string | undefined {
|
|
27
|
+
return latestAgentTracker?.getAgentForSession(sessionID)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isArgusAgent(sessionID: string): boolean {
|
|
31
|
+
return latestAgentTracker?.isArgusAgent(sessionID) ?? false
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
export type Hooks = Pick<
|
|
15
35
|
PluginHooks,
|
|
16
36
|
| "config"
|
|
37
|
+
| "chat.params"
|
|
38
|
+
| "chat.message"
|
|
17
39
|
| "experimental.chat.system.transform"
|
|
18
40
|
| "experimental.session.compacting"
|
|
19
41
|
| "tool.execute.after"
|
|
20
42
|
| "event"
|
|
21
43
|
>
|
|
22
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Creates the hook handlers for the Argus plugin.
|
|
47
|
+
*
|
|
48
|
+
* Context Delivery Strategy:
|
|
49
|
+
* - Prompt: Static agent identity (src/agents/*-prompt.ts) — methodology, personality, tool instructions
|
|
50
|
+
* - Hook: Dynamic state injection via experimental.chat.system.transform — audit progress, findings, phase
|
|
51
|
+
* - Skill-load: On-demand knowledge via argus_skill_load tool — vulnerability patterns, protocol knowledge
|
|
52
|
+
*
|
|
53
|
+
* The system.transform hook injects dynamic audit context only for Argus-family agents
|
|
54
|
+
* (argus, sentinel, pythia, scribe). Non-audit agents receive no injection.
|
|
55
|
+
*/
|
|
23
56
|
export function createHooks(args: {
|
|
24
57
|
config: ArgusConfig
|
|
25
58
|
managers: Managers
|
|
@@ -28,13 +61,17 @@ export function createHooks(args: {
|
|
|
28
61
|
}): Hooks {
|
|
29
62
|
const { config, managers, projectDir, isHookEnabled } = args
|
|
30
63
|
const { auditStateManager, backgroundManager } = managers
|
|
64
|
+
const agentTracker = createAgentTracker()
|
|
65
|
+
latestAgentTracker = agentTracker
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
67
|
+
const contextMonitor = createContextMonitor()
|
|
68
|
+
const sessionRecoveryHandler = createSessionRecoveryHandler(auditStateManager)
|
|
69
|
+
let auditStateGetter: (() => AuditState | null) | undefined
|
|
70
|
+
const toolErrorRecoveryHandler = createToolErrorRecoveryHandler(() => auditStateGetter?.() ?? null)
|
|
71
|
+
const outputTruncator = createToolOutputTruncator()
|
|
35
72
|
|
|
36
73
|
const { hook: eventHook, getAuditState, setAuditState } = createEventHookV2(projectDir, [
|
|
37
|
-
async ({ type, auditState, setAuditState: setState }) => {
|
|
74
|
+
async ({ type, sessionId, auditState, setAuditState: setState }) => {
|
|
38
75
|
if (type === "session.created") {
|
|
39
76
|
const recoveredState = await auditStateManager.load()
|
|
40
77
|
if (recoveredState) {
|
|
@@ -49,6 +86,9 @@ export function createHooks(args: {
|
|
|
49
86
|
}
|
|
50
87
|
|
|
51
88
|
if (type === "session.deleted") {
|
|
89
|
+
if (sessionId) {
|
|
90
|
+
agentTracker.clearSession(sessionId)
|
|
91
|
+
}
|
|
52
92
|
await auditStateManager.reset()
|
|
53
93
|
}
|
|
54
94
|
},
|
|
@@ -62,13 +102,50 @@ export function createHooks(args: {
|
|
|
62
102
|
},
|
|
63
103
|
])
|
|
64
104
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
auditStateGetter = getAuditState
|
|
106
|
+
|
|
107
|
+
const initialState = auditStateManager.get()
|
|
108
|
+
if (initialState) {
|
|
109
|
+
setAuditState(initialState)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const auditEnforcer = createAuditEnforcer()
|
|
69
113
|
|
|
70
|
-
|
|
71
|
-
|
|
114
|
+
const systemPromptHook = createSystemPromptHook({
|
|
115
|
+
getAuditState,
|
|
116
|
+
getAgentForSession: agentTracker.getAgentForSession,
|
|
117
|
+
isArgusAgent: agentTracker.isArgusAgent,
|
|
118
|
+
getContextPressure: (systemText: string) => {
|
|
119
|
+
const status = contextMonitor.getContextStatus(systemText, getAuditState())
|
|
120
|
+
return status.usage
|
|
121
|
+
},
|
|
122
|
+
getTokenBudget: getTokenBudgetForAgent,
|
|
123
|
+
getEnforcerReminder: auditEnforcer,
|
|
124
|
+
getReconBlock: () => buildReconContextBlock({
|
|
125
|
+
projectConfig: reconProjectConfig,
|
|
126
|
+
dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
|
|
127
|
+
auditArtifacts: detectAuditArtifacts(projectDir),
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
let reconProjectConfig: ProjectConfig | null = null
|
|
132
|
+
|
|
133
|
+
detectProject(projectDir)
|
|
134
|
+
.then((config) => {
|
|
135
|
+
reconProjectConfig = config
|
|
136
|
+
})
|
|
137
|
+
.catch(() => {
|
|
138
|
+
// Silent fallback — audit artifacts remain available
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const getReconContext = (): ReconContext => ({
|
|
142
|
+
projectConfig: reconProjectConfig,
|
|
143
|
+
dependencyRisks: reconProjectConfig?.dependencyRisks ?? [],
|
|
144
|
+
auditArtifacts: detectAuditArtifacts(projectDir),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const compactionHook = isHookEnabled("compaction")
|
|
148
|
+
? safeCreateHook(() => createCompactionHook(getAuditState, getReconContext), "compaction")
|
|
72
149
|
: undefined
|
|
73
150
|
|
|
74
151
|
const toolTrackingHook = isHookEnabled("tool-tracking")
|
|
@@ -79,9 +156,17 @@ export function createHooks(args: {
|
|
|
79
156
|
? safeCreateHook(() => eventHook, "event")
|
|
80
157
|
: undefined
|
|
81
158
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
159
|
+
return {
|
|
160
|
+
config: createConfigHandler(config, projectDir),
|
|
161
|
+
"chat.params": async (input) => {
|
|
162
|
+
agentTracker.chatParamsHook(input)
|
|
163
|
+
},
|
|
164
|
+
"chat.message": async (input) => {
|
|
165
|
+
agentTracker.chatMessageHook(input)
|
|
166
|
+
},
|
|
167
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
168
|
+
await systemPromptHook(input, output)
|
|
169
|
+
},
|
|
85
170
|
"experimental.session.compacting": compactionHook
|
|
86
171
|
? async (_input, output) => {
|
|
87
172
|
const block = await compactionHook({ summary: output.context.join("\n") })
|
package/src/create-tools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { patternCheckerTool } from "./tools/pattern-checker-tool"
|
|
|
8
8
|
import { soloditSearchTool } from "./tools/solodit-search-tool"
|
|
9
9
|
import { reportGeneratorTool } from "./tools/report-generator-tool"
|
|
10
10
|
import { syncKnowledgeTool } from "./tools/sync-knowledge-tool"
|
|
11
|
+
import { argusSkillLoadTool } from "./tools/argus-skill-load-tool"
|
|
11
12
|
|
|
12
13
|
export function createTools(
|
|
13
14
|
config: ArgusConfig,
|
|
@@ -18,6 +19,7 @@ export function createTools(
|
|
|
18
19
|
argus_forge_fuzz: forgeFuzzTool,
|
|
19
20
|
argus_analyze_contract: contractAnalyzerTool,
|
|
20
21
|
argus_check_patterns: patternCheckerTool,
|
|
22
|
+
argus_skill_load: argusSkillLoadTool,
|
|
21
23
|
argus_generate_report: reportGeneratorTool,
|
|
22
24
|
argus_sync_knowledge: syncKnowledgeTool,
|
|
23
25
|
}
|
|
@@ -1,15 +1,60 @@
|
|
|
1
1
|
import { createLogger } from "../../shared/logger"
|
|
2
|
+
import type { AuditState } from "../../state/types"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
solodit: "Check network connectivity or Solodit API status",
|
|
7
|
-
scvd: "Check SCVD API at https://api.scvd.dev — may be temporarily unavailable",
|
|
4
|
+
type ToolFallbackEntry = {
|
|
5
|
+
install: string
|
|
6
|
+
fallback: string
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
const
|
|
9
|
+
const TOOL_FALLBACKS: Record<string, ToolFallbackEntry> = {
|
|
10
|
+
slither: {
|
|
11
|
+
install: "pip install slither-analyzer",
|
|
12
|
+
fallback:
|
|
13
|
+
"Slither is unavailable. PROCEED with the audit using `argus_analyze_contract` for structural profiling and `argus_check_patterns` for vulnerability scanning. Note in the final report: \"Automated static analysis (Slither) was unavailable; manual review intensity increased.\"",
|
|
14
|
+
},
|
|
15
|
+
forge: {
|
|
16
|
+
install: "curl -L https://foundry.paradigm.xyz | bash && foundryup",
|
|
17
|
+
fallback:
|
|
18
|
+
"Foundry/Forge is unavailable. SKIP automated testing and fuzzing. Verify findings through manual code tracing and static analysis. Note in the final report: \"Dynamic testing (Forge) was unavailable; findings verified via manual analysis.\"",
|
|
19
|
+
},
|
|
20
|
+
solodit: {
|
|
21
|
+
install: "",
|
|
22
|
+
fallback:
|
|
23
|
+
"Solodit API is unreachable. PROCEED using `argus_check_patterns` with local vulnerability rules. Note in the final report: \"External vulnerability databases were inaccessible; research limited to local patterns.\"",
|
|
24
|
+
},
|
|
25
|
+
scvd: {
|
|
26
|
+
install: "",
|
|
27
|
+
fallback:
|
|
28
|
+
"SCVD API is unavailable. PROCEED with local patterns and Solodit search if available.",
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VIA_IR_HINT =
|
|
33
|
+
"Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
|
|
34
|
+
|
|
35
|
+
function isToolUnavailable(lowerResult: string): boolean {
|
|
36
|
+
return (
|
|
37
|
+
lowerResult.includes("enoent") ||
|
|
38
|
+
lowerResult.includes("not found") ||
|
|
39
|
+
lowerResult.includes("not installed")
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isToolError(lowerResult: string): boolean {
|
|
44
|
+
return (
|
|
45
|
+
isToolUnavailable(lowerResult) ||
|
|
46
|
+
lowerResult.includes("command failed") ||
|
|
47
|
+
lowerResult.includes("error:")
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveToolBase(tool: string): string {
|
|
52
|
+
return tool.replace("argus_", "").split("_")[0] ?? ""
|
|
53
|
+
}
|
|
11
54
|
|
|
12
|
-
export function createToolErrorRecoveryHandler(
|
|
55
|
+
export function createToolErrorRecoveryHandler(
|
|
56
|
+
getAuditState?: () => AuditState | null,
|
|
57
|
+
) {
|
|
13
58
|
const logger = createLogger()
|
|
14
59
|
|
|
15
60
|
return (toolResult: { tool: string; result: string }): string | null => {
|
|
@@ -27,22 +72,32 @@ export function createToolErrorRecoveryHandler() {
|
|
|
27
72
|
return `\n[Argus Recovery Hint] ${VIA_IR_HINT}`
|
|
28
73
|
}
|
|
29
74
|
|
|
30
|
-
|
|
31
|
-
lowerResult.includes("enoent") ||
|
|
32
|
-
lowerResult.includes("not found") ||
|
|
33
|
-
lowerResult.includes("command failed") ||
|
|
34
|
-
lowerResult.includes("error:")
|
|
75
|
+
if (!isToolError(lowerResult)) return null
|
|
35
76
|
|
|
36
|
-
|
|
77
|
+
const toolBase = resolveToolBase(tool)
|
|
78
|
+
const entry = TOOL_FALLBACKS[toolBase]
|
|
79
|
+
if (!entry) return null
|
|
37
80
|
|
|
38
|
-
const
|
|
39
|
-
|
|
81
|
+
const unavailable = isToolUnavailable(lowerResult)
|
|
82
|
+
|
|
83
|
+
if (unavailable && getAuditState) {
|
|
84
|
+
const state = getAuditState()
|
|
85
|
+
if (state) {
|
|
86
|
+
state.unavailableTools ??= []
|
|
87
|
+
if (!state.unavailableTools.includes(toolBase)) {
|
|
88
|
+
state.unavailableTools.push(toolBase)
|
|
89
|
+
logger.info(`Recorded ${toolBase} as unavailable — fallback activated`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
40
93
|
|
|
41
|
-
if (
|
|
42
|
-
logger.info(`Tool
|
|
43
|
-
return `\n[Argus
|
|
94
|
+
if (unavailable) {
|
|
95
|
+
logger.info(`Tool unavailable fallback for ${tool}`)
|
|
96
|
+
return `\n[Argus Fallback] ${entry.fallback}`
|
|
44
97
|
}
|
|
45
98
|
|
|
46
|
-
|
|
99
|
+
const installHint = entry.install ? ` (install: ${entry.install})` : ""
|
|
100
|
+
logger.info(`Tool error recovery hint for ${tool}`)
|
|
101
|
+
return `\n[Argus Recovery Hint] ${toolBase} error${installHint}. ${entry.fallback}`
|
|
47
102
|
}
|
|
48
103
|
}
|
|
@@ -7,7 +7,7 @@ import { createLogger } from "../../shared/logger";
|
|
|
7
7
|
|
|
8
8
|
const STATE_FILE_DIR = ".opencode";
|
|
9
9
|
const STATE_FILE_NAME = "argus-state.json";
|
|
10
|
-
const STATE_VERSION = "
|
|
10
|
+
const STATE_VERSION = "2";
|
|
11
11
|
|
|
12
12
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
13
13
|
return typeof value === "object" && value !== null;
|
|
@@ -39,9 +39,11 @@ function isPersistentAuditState(value: unknown): value is PersistentAuditState {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const hasSupportedVersion = value.version === "1" || value.version === "2";
|
|
43
|
+
|
|
42
44
|
return (
|
|
43
45
|
typeof value.savedAt === "number" &&
|
|
44
|
-
|
|
46
|
+
hasSupportedVersion &&
|
|
45
47
|
typeof value.filePath === "string"
|
|
46
48
|
);
|
|
47
49
|
}
|
|
@@ -69,7 +71,17 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
69
71
|
return null;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
const { savedAt: _savedAt, version
|
|
74
|
+
const { savedAt: _savedAt, version, filePath: _filePath, ...state } = parsed;
|
|
75
|
+
|
|
76
|
+
if (version === "1") {
|
|
77
|
+
if (!state.soloditResults) {
|
|
78
|
+
state.soloditResults = [];
|
|
79
|
+
}
|
|
80
|
+
if (!state.fuzzCounterexamples) {
|
|
81
|
+
state.fuzzCounterexamples = [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
73
85
|
currentState = state;
|
|
74
86
|
return currentState;
|
|
75
87
|
} catch (_error) {
|
|
@@ -77,20 +89,31 @@ export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
92
|
+
let saveInFlight = false;
|
|
93
|
+
|
|
80
94
|
async function save(state: AuditState): Promise<void> {
|
|
81
95
|
currentState = state;
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
savedAt: Date.now(),
|
|
86
|
-
version: STATE_VERSION,
|
|
87
|
-
filePath: stateFilePath,
|
|
88
|
-
};
|
|
97
|
+
if (saveInFlight) return;
|
|
98
|
+
saveInFlight = true;
|
|
89
99
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
try {
|
|
101
|
+
const persistentState: PersistentAuditState = {
|
|
102
|
+
...state,
|
|
103
|
+
savedAt: Date.now(),
|
|
104
|
+
version: STATE_VERSION,
|
|
105
|
+
filePath: stateFilePath,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const tempFilePath = `${stateFilePath}.${Date.now()}.tmp`;
|
|
109
|
+
await mkdir(dirname(stateFilePath), { recursive: true });
|
|
110
|
+
await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
|
|
111
|
+
await rename(tempFilePath, stateFilePath);
|
|
112
|
+
} catch {
|
|
113
|
+
// Non-critical: state persistence is best-effort
|
|
114
|
+
} finally {
|
|
115
|
+
saveInFlight = false;
|
|
116
|
+
}
|
|
94
117
|
}
|
|
95
118
|
|
|
96
119
|
function get(): AuditState {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
type ChatParamsInput = Parameters<NonNullable<PluginHooks["chat.params"]>>[0] & {
|
|
4
|
+
agent?: string
|
|
5
|
+
}
|
|
6
|
+
type ChatMessageInput = Parameters<NonNullable<PluginHooks["chat.message"]>>[0]
|
|
7
|
+
|
|
8
|
+
const ARGUS_FAMILY = new Set(["argus", "sentinel", "pythia", "scribe"])
|
|
9
|
+
|
|
10
|
+
export type AgentTracker = ReturnType<typeof createAgentTracker>
|
|
11
|
+
|
|
12
|
+
export function createAgentTracker() {
|
|
13
|
+
const sessions = new Map<string, string>()
|
|
14
|
+
|
|
15
|
+
const trackSession = (sessionID: string, agent?: string): void => {
|
|
16
|
+
if (!agent) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
sessions.set(sessionID, agent)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
chatParamsHook: (input: ChatParamsInput): void => {
|
|
25
|
+
trackSession(input.sessionID, input.agent)
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
chatMessageHook: (input: ChatMessageInput): void => {
|
|
29
|
+
trackSession(input.sessionID, input.agent)
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getAgentForSession: (sessionID: string): string | undefined => {
|
|
33
|
+
return sessions.get(sessionID)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
isArgusAgent: (sessionID: string): boolean => {
|
|
37
|
+
const agent = sessions.get(sessionID)
|
|
38
|
+
if (!agent) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ARGUS_FAMILY.has(agent)
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
clearSession: (sessionID: string): void => {
|
|
46
|
+
sessions.delete(sessionID)
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
getTrackedSessions: (): Map<string, string> => {
|
|
50
|
+
return sessions
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,50 +1,59 @@
|
|
|
1
1
|
import type { AuditState, FindingSeverity } from "../state/types"
|
|
2
|
+
import type { ReconContext } from "./recon-context-builder"
|
|
3
|
+
import { buildReconContextBlock } from "./recon-context-builder"
|
|
2
4
|
|
|
3
|
-
/**
|
|
4
|
-
* Creates a compaction hook that serializes audit state into XML format
|
|
5
|
-
* so findings survive context window compression.
|
|
6
|
-
*
|
|
7
|
-
* The returned hook is called by OpenCode's `experimental.session.compacting`
|
|
8
|
-
* event, receiving `{ summary: string }` and returning the enriched summary.
|
|
9
|
-
*/
|
|
10
5
|
export function createCompactionHook(
|
|
11
|
-
getAuditState: () => AuditState | null
|
|
6
|
+
getAuditState: () => AuditState | null,
|
|
7
|
+
getReconContext?: () => ReconContext | null,
|
|
12
8
|
): (input: { summary: string }) => Promise<string | null> {
|
|
13
9
|
return async (_input: { summary: string }): Promise<string | null> => {
|
|
14
10
|
const state = getAuditState()
|
|
15
|
-
if (!state) {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
11
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
|
|
14
|
+
if (state) {
|
|
15
|
+
const severityCounts: Record<FindingSeverity, number> = {
|
|
16
|
+
Critical: 0,
|
|
17
|
+
High: 0,
|
|
18
|
+
Medium: 0,
|
|
19
|
+
Low: 0,
|
|
20
|
+
Informational: 0,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const finding of state.findings) {
|
|
24
|
+
severityCounts[finding.severity]++
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const toolNames = state.toolsExecuted.map((t) => t.tool).join(", ")
|
|
28
|
+
const contracts = state.contractsReviewed.join(", ")
|
|
29
|
+
const started = new Date(state.startTime).toISOString()
|
|
30
|
+
|
|
31
|
+
parts.push(
|
|
32
|
+
[
|
|
33
|
+
"<argus-audit-state>",
|
|
34
|
+
`Phase: ${state.currentPhase}`,
|
|
35
|
+
`Contracts Reviewed: ${contracts}`,
|
|
36
|
+
"Findings:",
|
|
37
|
+
` Critical: ${severityCounts.Critical}`,
|
|
38
|
+
` High: ${severityCounts.High}`,
|
|
39
|
+
` Medium: ${severityCounts.Medium}`,
|
|
40
|
+
` Low: ${severityCounts.Low}`,
|
|
41
|
+
` Informational: ${severityCounts.Informational}`,
|
|
42
|
+
`Tools Executed: ${toolNames}`,
|
|
43
|
+
`Started: ${started}`,
|
|
44
|
+
"</argus-audit-state>",
|
|
45
|
+
].join("\n"),
|
|
46
|
+
)
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
if (getReconContext) {
|
|
50
|
+
const recon = getReconContext()
|
|
51
|
+
if (recon) {
|
|
52
|
+
const reconBlock = buildReconContextBlock(recon)
|
|
53
|
+
if (reconBlock) parts.push(reconBlock)
|
|
54
|
+
}
|
|
29
55
|
}
|
|
30
56
|
|
|
31
|
-
|
|
32
|
-
const contracts = state.contractsReviewed.join(", ")
|
|
33
|
-
const started = new Date(state.startTime).toISOString()
|
|
34
|
-
|
|
35
|
-
return [
|
|
36
|
-
"<argus-audit-state>",
|
|
37
|
-
`Phase: ${state.currentPhase}`,
|
|
38
|
-
`Contracts Reviewed: ${contracts}`,
|
|
39
|
-
"Findings:",
|
|
40
|
-
` Critical: ${severityCounts.Critical}`,
|
|
41
|
-
` High: ${severityCounts.High}`,
|
|
42
|
-
` Medium: ${severityCounts.Medium}`,
|
|
43
|
-
` Low: ${severityCounts.Low}`,
|
|
44
|
-
` Informational: ${severityCounts.Informational}`,
|
|
45
|
-
`Tools Executed: ${toolNames}`,
|
|
46
|
-
`Started: ${started}`,
|
|
47
|
-
"</argus-audit-state>",
|
|
48
|
-
].join("\n")
|
|
57
|
+
return parts.length > 0 ? parts.join("\n") : null
|
|
49
58
|
}
|
|
50
59
|
}
|
|
@@ -93,6 +93,7 @@ export function createConfigHandler(
|
|
|
93
93
|
argus_forge_fuzz: "allow",
|
|
94
94
|
argus_analyze_contract: "allow",
|
|
95
95
|
argus_check_patterns: "allow",
|
|
96
|
+
argus_skill_load: "allow",
|
|
96
97
|
skill: "allow",
|
|
97
98
|
},
|
|
98
99
|
},
|
|
@@ -104,6 +105,7 @@ export function createConfigHandler(
|
|
|
104
105
|
permission: {
|
|
105
106
|
argus_solodit_search: "allow",
|
|
106
107
|
argus_check_patterns: "allow",
|
|
108
|
+
argus_skill_load: "allow",
|
|
107
109
|
skill: "allow",
|
|
108
110
|
},
|
|
109
111
|
},
|
|
@@ -114,6 +116,7 @@ export function createConfigHandler(
|
|
|
114
116
|
prompt: SCRIBE_PROMPT,
|
|
115
117
|
permission: {
|
|
116
118
|
argus_generate_report: "allow",
|
|
119
|
+
argus_skill_load: "allow",
|
|
117
120
|
skill: "allow",
|
|
118
121
|
},
|
|
119
122
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context budget allocation for Argus agents.
|
|
3
|
+
*
|
|
4
|
+
* Provides per-agent token budgets for system-prompt injection sizing.
|
|
5
|
+
* Under context pressure (>70%), budgets are reduced by 50% to prevent
|
|
6
|
+
* context window overflow during long audits.
|
|
7
|
+
*
|
|
8
|
+
* Decoupled from system-prompt-hook.ts — consumed by the hook when available.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ARGUS_BUDGET = 2000
|
|
12
|
+
const SUBAGENT_BUDGET = 1000
|
|
13
|
+
const PRESSURE_THRESHOLD = 0.70
|
|
14
|
+
const PRESSURE_REDUCTION = 0.5
|
|
15
|
+
|
|
16
|
+
const ARGUS_AGENTS = new Set(["argus"])
|
|
17
|
+
const SUBAGENTS = new Set(["sentinel", "pythia", "scribe"])
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns the token budget for a given agent, adjusted for context pressure.
|
|
21
|
+
*
|
|
22
|
+
* @param agent - Agent name (e.g. "argus", "sentinel", "pythia", "scribe")
|
|
23
|
+
* @param contextPressure - Current context usage ratio (0.0–1.0), from ContextMonitor
|
|
24
|
+
* @returns Token budget in tokens. 0 for non-Argus agents.
|
|
25
|
+
*/
|
|
26
|
+
export function getTokenBudgetForAgent(
|
|
27
|
+
agent: string,
|
|
28
|
+
contextPressure: number = 0,
|
|
29
|
+
): number {
|
|
30
|
+
let budget: number
|
|
31
|
+
|
|
32
|
+
if (ARGUS_AGENTS.has(agent)) {
|
|
33
|
+
budget = ARGUS_BUDGET
|
|
34
|
+
} else if (SUBAGENTS.has(agent)) {
|
|
35
|
+
budget = SUBAGENT_BUDGET
|
|
36
|
+
} else {
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (contextPressure > PRESSURE_THRESHOLD) {
|
|
41
|
+
budget = Math.floor(budget * PRESSURE_REDUCTION)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return budget
|
|
45
|
+
}
|
package/src/hooks/event-hook.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuditState } from "../state/types"
|
|
2
2
|
import { createAuditState } from "../state/audit-state"
|
|
3
|
+
import { createLogger } from "../shared/logger"
|
|
3
4
|
|
|
4
5
|
export type EventHookFn = (input: {
|
|
5
6
|
event: { type: string; sessionId?: string }
|
|
@@ -38,8 +39,8 @@ export function createEventHook(projectDir?: string): {
|
|
|
38
39
|
|
|
39
40
|
case "session.idle": {
|
|
40
41
|
if (currentAuditState) {
|
|
41
|
-
|
|
42
|
-
`[
|
|
42
|
+
createLogger().debug(
|
|
43
|
+
`[state] Session idle — phase: ${currentAuditState.currentPhase}, findings: ${currentAuditState.findings.length}, contracts: ${currentAuditState.contractsReviewed.length}`
|
|
43
44
|
)
|
|
44
45
|
}
|
|
45
46
|
break
|
|
@@ -47,8 +48,8 @@ export function createEventHook(projectDir?: string): {
|
|
|
47
48
|
|
|
48
49
|
case "session.error": {
|
|
49
50
|
if (currentAuditState) {
|
|
50
|
-
|
|
51
|
-
`
|
|
51
|
+
createLogger().error(
|
|
52
|
+
`Session error — state snapshot: ${JSON.stringify({
|
|
52
53
|
sessionId: currentAuditState.sessionId,
|
|
53
54
|
phase: currentAuditState.currentPhase,
|
|
54
55
|
findingsCount: currentAuditState.findings.length,
|
|
@@ -3,6 +3,7 @@ import path from "node:path"
|
|
|
3
3
|
import { ScvdClient } from "../knowledge/scvd-client"
|
|
4
4
|
import { syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
|
|
5
5
|
import type { ArgusConfig } from "../config/types"
|
|
6
|
+
import { createLogger } from "../shared/logger"
|
|
6
7
|
|
|
7
8
|
export type KnowledgeSyncDependencies = {
|
|
8
9
|
createClient?: (apiUrl: string) => unknown
|
|
@@ -18,7 +19,7 @@ function defaultDependencies(): Required<KnowledgeSyncDependencies> {
|
|
|
18
19
|
syncIncrementalFn: async (client: unknown, indexPath: string) =>
|
|
19
20
|
syncIncremental(client as ScvdClient, indexPath),
|
|
20
21
|
log: (message: string) => {
|
|
21
|
-
|
|
22
|
+
createLogger().info(message)
|
|
22
23
|
},
|
|
23
24
|
}
|
|
24
25
|
}
|