solidity-argus 0.1.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/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +43 -0
- package/skills/INVENTORY.md +79 -0
- package/skills/README.md +56 -0
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
- package/skills/checklists/general-audit/SKILL.md +433 -0
- package/skills/methodology/audit-workflow/SKILL.md +129 -0
- package/skills/methodology/report-template/SKILL.md +190 -0
- package/skills/methodology/severity-classification/SKILL.md +179 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
- package/skills/references/exploit-reference/SKILL.md +259 -0
- package/skills/references/smartbugs-examples/SKILL.md +296 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
- package/src/agents/argus-prompt.ts +407 -0
- package/src/agents/pythia-prompt.ts +134 -0
- package/src/agents/scribe-prompt.ts +87 -0
- package/src/agents/sentinel-prompt.ts +133 -0
- package/src/cli/cli-program.ts +67 -0
- package/src/cli/commands/doctor.ts +83 -0
- package/src/cli/commands/init.ts +46 -0
- package/src/cli/commands/install.ts +55 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/tui-prompts.ts +75 -0
- package/src/cli/types.ts +9 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.ts +36 -0
- package/src/config/schema.ts +82 -0
- package/src/config/types.ts +4 -0
- package/src/constants/defaults.ts +6 -0
- package/src/create-hooks.ts +84 -0
- package/src/create-managers.ts +26 -0
- package/src/create-tools.ts +30 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
- package/src/features/audit-enforcer/index.ts +1 -0
- package/src/features/background-agent/background-manager.ts +200 -0
- package/src/features/background-agent/index.ts +1 -0
- package/src/features/context-monitor/context-monitor.ts +48 -0
- package/src/features/context-monitor/index.ts +4 -0
- package/src/features/context-monitor/tool-output-truncator.ts +17 -0
- package/src/features/error-recovery/index.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +27 -0
- package/src/features/error-recovery/tool-error-recovery.ts +35 -0
- package/src/features/index.ts +5 -0
- package/src/features/persistent-state/audit-state-manager.ts +121 -0
- package/src/features/persistent-state/index.ts +1 -0
- package/src/hooks/compaction-hook.ts +50 -0
- package/src/hooks/config-handler.ts +116 -0
- package/src/hooks/event-hook-v2.ts +93 -0
- package/src/hooks/event-hook.ts +74 -0
- package/src/hooks/hook-system.ts +9 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/knowledge-sync-hook.ts +57 -0
- package/src/hooks/safe-create-hook.ts +15 -0
- package/src/hooks/system-prompt-hook.ts +126 -0
- package/src/hooks/tool-tracking-hook.ts +234 -0
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +36 -0
- package/src/knowledge/scvd-client.ts +242 -0
- package/src/knowledge/scvd-index.ts +183 -0
- package/src/knowledge/scvd-sync.ts +85 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/types.ts +85 -0
- package/src/plugin-interface.ts +38 -0
- package/src/shared/binary-utils.ts +63 -0
- package/src/shared/deep-merge.ts +71 -0
- package/src/shared/file-utils.ts +56 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/jsonc-parser.ts +39 -0
- package/src/shared/logger.ts +36 -0
- package/src/state/audit-state.ts +27 -0
- package/src/state/finding-store.ts +126 -0
- package/src/state/plugin-state.ts +14 -0
- package/src/state/types.ts +61 -0
- package/src/tools/contract-analyzer-tool.ts +184 -0
- package/src/tools/forge-fuzz-tool.ts +311 -0
- package/src/tools/forge-test-tool.ts +397 -0
- package/src/tools/pattern-checker-tool.ts +337 -0
- package/src/tools/report-generator-tool.ts +308 -0
- package/src/tools/slither-tool.ts +465 -0
- package/src/tools/solodit-search-tool.ts +131 -0
- package/src/tools/sync-knowledge-tool.ts +116 -0
- package/src/utils/project-detector.ts +133 -0
- package/src/utils/solidity-parser.ts +174 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Hooks as PluginHooks } from "@opencode-ai/plugin"
|
|
2
|
+
import type { ArgusConfig } from "./config/types"
|
|
3
|
+
import type { Managers } from "./managers/types"
|
|
4
|
+
import type { HookName } from "./hooks/types"
|
|
5
|
+
import { createAuditState } from "./state/audit-state"
|
|
6
|
+
import { createConfigHandler } from "./hooks/config-handler"
|
|
7
|
+
import { createSystemPromptHook } from "./hooks/system-prompt-hook"
|
|
8
|
+
import { createCompactionHook } from "./hooks/compaction-hook"
|
|
9
|
+
import { createToolTrackingHook } from "./hooks/tool-tracking-hook"
|
|
10
|
+
import { createEventHook } from "./hooks/event-hook"
|
|
11
|
+
import { safeCreateHook } from "./hooks/safe-create-hook"
|
|
12
|
+
|
|
13
|
+
export type Hooks = Pick<
|
|
14
|
+
PluginHooks,
|
|
15
|
+
| "config"
|
|
16
|
+
| "experimental.chat.system.transform"
|
|
17
|
+
| "experimental.session.compacting"
|
|
18
|
+
| "tool.execute.after"
|
|
19
|
+
| "event"
|
|
20
|
+
>
|
|
21
|
+
|
|
22
|
+
export function createHooks(args: {
|
|
23
|
+
config: ArgusConfig
|
|
24
|
+
managers: Managers
|
|
25
|
+
projectDir: string
|
|
26
|
+
isHookEnabled: (name: HookName) => boolean
|
|
27
|
+
}): Hooks {
|
|
28
|
+
const { config, projectDir, isHookEnabled } = args
|
|
29
|
+
|
|
30
|
+
const { state: auditState, store: findingStore } = createAuditState(projectDir)
|
|
31
|
+
const { hook: eventHook, getAuditState, setAuditState } = createEventHook(projectDir)
|
|
32
|
+
setAuditState(auditState)
|
|
33
|
+
|
|
34
|
+
const systemPromptHook = isHookEnabled("system-prompt")
|
|
35
|
+
? safeCreateHook(
|
|
36
|
+
() => createSystemPromptHook(getAuditState),
|
|
37
|
+
"system-prompt"
|
|
38
|
+
)
|
|
39
|
+
: undefined
|
|
40
|
+
|
|
41
|
+
const compactionHook = isHookEnabled("compaction")
|
|
42
|
+
? safeCreateHook(() => createCompactionHook(getAuditState), "compaction")
|
|
43
|
+
: undefined
|
|
44
|
+
|
|
45
|
+
const toolTrackingHook = isHookEnabled("tool-tracking")
|
|
46
|
+
? safeCreateHook(
|
|
47
|
+
() => createToolTrackingHook(auditState, findingStore),
|
|
48
|
+
"tool-tracking"
|
|
49
|
+
)
|
|
50
|
+
: undefined
|
|
51
|
+
|
|
52
|
+
const safeEventHook = isHookEnabled("event")
|
|
53
|
+
? safeCreateHook(() => eventHook, "event")
|
|
54
|
+
: undefined
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
config: createConfigHandler(config),
|
|
58
|
+
"experimental.chat.system.transform": systemPromptHook
|
|
59
|
+
? async (_input, output) => {
|
|
60
|
+
const block = await systemPromptHook({
|
|
61
|
+
system: output.system.join("\n\n"),
|
|
62
|
+
cwd: projectDir,
|
|
63
|
+
})
|
|
64
|
+
if (block) output.system.push(block)
|
|
65
|
+
}
|
|
66
|
+
: undefined,
|
|
67
|
+
"experimental.session.compacting": compactionHook
|
|
68
|
+
? async (_input, output) => {
|
|
69
|
+
const block = await compactionHook({ summary: output.context.join("\n") })
|
|
70
|
+
if (block) output.context.push(block)
|
|
71
|
+
}
|
|
72
|
+
: undefined,
|
|
73
|
+
"tool.execute.after": toolTrackingHook
|
|
74
|
+
? async (input, output) => {
|
|
75
|
+
await toolTrackingHook({
|
|
76
|
+
tool: input.tool,
|
|
77
|
+
args: input.args,
|
|
78
|
+
result: output.output,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
82
|
+
event: safeEventHook,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ArgusConfig } from "./config/types"
|
|
2
|
+
import type { Managers } from "./managers/types"
|
|
3
|
+
import { createBackgroundManager } from "./features/background-agent/background-manager"
|
|
4
|
+
import { createAuditStateManager } from "./features/persistent-state/audit-state-manager"
|
|
5
|
+
import { createLogger } from "./shared/logger"
|
|
6
|
+
|
|
7
|
+
export function createManagers(args: {
|
|
8
|
+
projectDir: string
|
|
9
|
+
config: ArgusConfig
|
|
10
|
+
}): Managers {
|
|
11
|
+
const { projectDir } = args
|
|
12
|
+
const logger = createLogger()
|
|
13
|
+
|
|
14
|
+
const backgroundManager = createBackgroundManager(
|
|
15
|
+
async (agentName: string, prompt: string) => {
|
|
16
|
+
logger.warn(
|
|
17
|
+
`Background dispatch not wired: ${agentName} (${prompt.slice(0, 50)}...)`,
|
|
18
|
+
)
|
|
19
|
+
return `noop-${Date.now()}`
|
|
20
|
+
},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const auditStateManager = createAuditStateManager(projectDir)
|
|
24
|
+
|
|
25
|
+
return { backgroundManager, auditStateManager }
|
|
26
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@opencode-ai/plugin"
|
|
2
|
+
import type { ArgusConfig } from "./config/types"
|
|
3
|
+
import { slitherTool } from "./tools/slither-tool"
|
|
4
|
+
import { forgeTestTool } from "./tools/forge-test-tool"
|
|
5
|
+
import { forgeFuzzTool } from "./tools/forge-fuzz-tool"
|
|
6
|
+
import { contractAnalyzerTool } from "./tools/contract-analyzer-tool"
|
|
7
|
+
import { patternCheckerTool } from "./tools/pattern-checker-tool"
|
|
8
|
+
import { soloditSearchTool } from "./tools/solodit-search-tool"
|
|
9
|
+
import { reportGeneratorTool } from "./tools/report-generator-tool"
|
|
10
|
+
import { syncKnowledgeTool } from "./tools/sync-knowledge-tool"
|
|
11
|
+
|
|
12
|
+
export function createTools(
|
|
13
|
+
config: ArgusConfig,
|
|
14
|
+
): Record<string, ToolDefinition> {
|
|
15
|
+
const tools: Record<string, ToolDefinition> = {
|
|
16
|
+
argus_slither_analyze: slitherTool,
|
|
17
|
+
argus_forge_test: forgeTestTool,
|
|
18
|
+
argus_forge_fuzz: forgeFuzzTool,
|
|
19
|
+
argus_analyze_contract: contractAnalyzerTool,
|
|
20
|
+
argus_check_patterns: patternCheckerTool,
|
|
21
|
+
argus_generate_report: reportGeneratorTool,
|
|
22
|
+
argus_sync_knowledge: syncKnowledgeTool,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (config.solodit?.enabled !== false) {
|
|
26
|
+
tools.argus_solodit_search = soloditSearchTool
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return tools
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AuditPhase, AuditState } from "../../state/types"
|
|
2
|
+
|
|
3
|
+
const PHASE_ORDER: AuditPhase[] = [
|
|
4
|
+
"reconnaissance",
|
|
5
|
+
"scanning",
|
|
6
|
+
"manual-review",
|
|
7
|
+
"attack-surface",
|
|
8
|
+
"research",
|
|
9
|
+
"testing",
|
|
10
|
+
"reporting",
|
|
11
|
+
"complete",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
function getNextPhase(current: AuditPhase): AuditPhase | null {
|
|
15
|
+
const idx = PHASE_ORDER.indexOf(current)
|
|
16
|
+
if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null
|
|
17
|
+
return PHASE_ORDER[idx + 1] ?? null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createAuditEnforcer() {
|
|
21
|
+
return (auditState: AuditState | null): string | null => {
|
|
22
|
+
if (!auditState) return null
|
|
23
|
+
if (auditState.currentPhase === "complete") return null
|
|
24
|
+
|
|
25
|
+
const nextPhase = getNextPhase(auditState.currentPhase)
|
|
26
|
+
if (!nextPhase) return null
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
`[Argus Audit Enforcer] Audit in progress — current phase: ${auditState.currentPhase}.`,
|
|
30
|
+
`Next phase: ${nextPhase}. Do not stop until audit is complete.`,
|
|
31
|
+
`Progress: ${auditState.findings.length} findings, ${auditState.contractsReviewed.length} contracts reviewed.`,
|
|
32
|
+
].join(" ")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createAuditEnforcer } from "./audit-enforcer"
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { BackgroundManager } from "../../managers/types";
|
|
2
|
+
import { createLogger } from "../../shared/logger";
|
|
3
|
+
|
|
4
|
+
type TaskStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
|
5
|
+
type CompletionCallback = (taskId: string, result: unknown) => void;
|
|
6
|
+
|
|
7
|
+
export interface BackgroundTaskOptions {
|
|
8
|
+
priority?: number;
|
|
9
|
+
max_concurrent?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BackgroundManagerWithTaskCallbacks extends BackgroundManager {
|
|
13
|
+
dispatch(agentName: string, prompt: string, options?: BackgroundTaskOptions): string;
|
|
14
|
+
onComplete(callback: CompletionCallback): void;
|
|
15
|
+
onComplete(taskId: string, callback: CompletionCallback): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TaskInfo {
|
|
19
|
+
status: TaskStatus;
|
|
20
|
+
agentName: string;
|
|
21
|
+
prompt: string;
|
|
22
|
+
options?: BackgroundTaskOptions;
|
|
23
|
+
result?: unknown;
|
|
24
|
+
error?: unknown;
|
|
25
|
+
callbacks: Set<CompletionCallback>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type Dispatcher = (
|
|
29
|
+
agentName: string,
|
|
30
|
+
prompt: string,
|
|
31
|
+
options?: BackgroundTaskOptions,
|
|
32
|
+
) => Promise<string>;
|
|
33
|
+
|
|
34
|
+
export function createBackgroundManager(
|
|
35
|
+
dispatcher: Dispatcher,
|
|
36
|
+
): BackgroundManagerWithTaskCallbacks {
|
|
37
|
+
const logger = createLogger();
|
|
38
|
+
const tasks = new Map<string, TaskInfo>();
|
|
39
|
+
const queue: string[] = [];
|
|
40
|
+
const globalCallbacks = new Set<CompletionCallback>();
|
|
41
|
+
|
|
42
|
+
let runningCount = 0;
|
|
43
|
+
let maxConcurrent = 3;
|
|
44
|
+
let taskCount = 0;
|
|
45
|
+
|
|
46
|
+
function invokeCallbacks(taskId: string, result: unknown): void {
|
|
47
|
+
const task = tasks.get(taskId);
|
|
48
|
+
if (!task) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const callback of globalCallbacks) {
|
|
53
|
+
callback(taskId, result);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const callback of task.callbacks) {
|
|
57
|
+
callback(taskId, result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
task.callbacks.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function processQueue(): void {
|
|
64
|
+
while (runningCount < maxConcurrent && queue.length > 0) {
|
|
65
|
+
const nextTaskId = queue.shift();
|
|
66
|
+
|
|
67
|
+
if (!nextTaskId) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const task = tasks.get(nextTaskId);
|
|
72
|
+
if (!task || task.status === "cancelled") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
task.status = "running";
|
|
77
|
+
runningCount += 1;
|
|
78
|
+
|
|
79
|
+
dispatcher(task.agentName, task.prompt, task.options)
|
|
80
|
+
.then((result) => {
|
|
81
|
+
const currentTask = tasks.get(nextTaskId);
|
|
82
|
+
|
|
83
|
+
if (!currentTask || currentTask.status === "cancelled") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
currentTask.status = "completed";
|
|
88
|
+
currentTask.result = result;
|
|
89
|
+
invokeCallbacks(nextTaskId, result);
|
|
90
|
+
})
|
|
91
|
+
.catch((error: unknown) => {
|
|
92
|
+
const currentTask = tasks.get(nextTaskId);
|
|
93
|
+
|
|
94
|
+
if (!currentTask || currentTask.status === "cancelled") {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
currentTask.status = "failed";
|
|
99
|
+
currentTask.error = error;
|
|
100
|
+
logger.error(`Background task failed: ${nextTaskId}`, error);
|
|
101
|
+
})
|
|
102
|
+
.finally(() => {
|
|
103
|
+
runningCount = Math.max(0, runningCount - 1);
|
|
104
|
+
processQueue();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function dispatch(
|
|
110
|
+
agentName: string,
|
|
111
|
+
prompt: string,
|
|
112
|
+
options?: BackgroundTaskOptions,
|
|
113
|
+
): string {
|
|
114
|
+
if (typeof options?.max_concurrent === "number" && options.max_concurrent > 0) {
|
|
115
|
+
maxConcurrent = options.max_concurrent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
taskCount += 1;
|
|
119
|
+
const taskId = `task-${taskCount}`;
|
|
120
|
+
|
|
121
|
+
tasks.set(taskId, {
|
|
122
|
+
status: "queued",
|
|
123
|
+
agentName,
|
|
124
|
+
prompt,
|
|
125
|
+
options,
|
|
126
|
+
callbacks: new Set<CompletionCallback>(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
queue.push(taskId);
|
|
130
|
+
processQueue();
|
|
131
|
+
|
|
132
|
+
return taskId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cancel(taskId: string): void {
|
|
136
|
+
const task = tasks.get(taskId);
|
|
137
|
+
if (!task) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
task.status = "cancelled";
|
|
142
|
+
|
|
143
|
+
const queuedTaskIndex = queue.indexOf(taskId);
|
|
144
|
+
if (queuedTaskIndex >= 0) {
|
|
145
|
+
queue.splice(queuedTaskIndex, 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getResult(taskId: string): Promise<unknown> {
|
|
150
|
+
const task = tasks.get(taskId);
|
|
151
|
+
if (!task || task.status !== "completed") {
|
|
152
|
+
return Promise.resolve(undefined);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Promise.resolve(task.result);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function onComplete(taskIdOrCallback: string | CompletionCallback, callback?: CompletionCallback): void {
|
|
159
|
+
if (typeof taskIdOrCallback === "function") {
|
|
160
|
+
globalCallbacks.add(taskIdOrCallback);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!callback) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const task = tasks.get(taskIdOrCallback);
|
|
169
|
+
if (!task) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (task.status === "completed") {
|
|
174
|
+
callback(taskIdOrCallback, task.result);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
task.callbacks.add(callback);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getActiveCount(): number {
|
|
182
|
+
let activeCount = 0;
|
|
183
|
+
|
|
184
|
+
for (const task of tasks.values()) {
|
|
185
|
+
if (task.status === "queued" || task.status === "running") {
|
|
186
|
+
activeCount += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return activeCount;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
dispatch,
|
|
195
|
+
cancel,
|
|
196
|
+
getResult,
|
|
197
|
+
onComplete,
|
|
198
|
+
getActiveCount,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./background-manager";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AuditState } from "../../state/types"
|
|
2
|
+
import { createLogger } from "../../shared/logger"
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_TOKENS = 200_000
|
|
5
|
+
const REMINDER_THRESHOLD = 0.70
|
|
6
|
+
const COMPACTION_THRESHOLD = 0.85
|
|
7
|
+
|
|
8
|
+
export interface ContextMonitorConfig {
|
|
9
|
+
maxTokens?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createContextMonitor(config: ContextMonitorConfig = {}) {
|
|
13
|
+
const logger = createLogger()
|
|
14
|
+
const maxTokens = config.maxTokens ?? DEFAULT_MAX_TOKENS
|
|
15
|
+
|
|
16
|
+
function estimateTokens(text: string): number {
|
|
17
|
+
return Math.ceil(text.length / 4)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getContextStatus(
|
|
21
|
+
systemText: string,
|
|
22
|
+
auditState: AuditState | null,
|
|
23
|
+
): { usage: number; reminder: string | null; shouldCompact: boolean } {
|
|
24
|
+
const tokens = estimateTokens(systemText)
|
|
25
|
+
const usage = tokens / maxTokens
|
|
26
|
+
|
|
27
|
+
if (usage >= COMPACTION_THRESHOLD) {
|
|
28
|
+
logger.info(`Context at ${Math.round(usage * 100)}% — triggering compaction`)
|
|
29
|
+
return {
|
|
30
|
+
usage,
|
|
31
|
+
reminder: `[Argus Context Warning] Context window at ${Math.round(usage * 100)}%. Compaction triggered. Prioritize critical findings.`,
|
|
32
|
+
shouldCompact: true,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (usage >= REMINDER_THRESHOLD) {
|
|
37
|
+
return {
|
|
38
|
+
usage,
|
|
39
|
+
reminder: `[Argus Context Notice] Context at ${Math.round(usage * 100)}% — maintain audit thoroughness, be concise.`,
|
|
40
|
+
shouldCompact: false,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { usage, reminder: null, shouldCompact: false }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { estimateTokens, getContextStatus }
|
|
48
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const DEFAULT_MAX_CHARS = 50_000
|
|
2
|
+
const MIN_CHARS = 1000
|
|
3
|
+
|
|
4
|
+
export interface TruncatorConfig {
|
|
5
|
+
maxChars?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createToolOutputTruncator(config: TruncatorConfig = {}) {
|
|
9
|
+
const maxChars = Math.max(config.maxChars ?? DEFAULT_MAX_CHARS, MIN_CHARS)
|
|
10
|
+
|
|
11
|
+
return (output: string): string => {
|
|
12
|
+
if (output.length <= maxChars) return output
|
|
13
|
+
|
|
14
|
+
const truncated = output.slice(0, maxChars)
|
|
15
|
+
return `${truncated}\n\n[Truncated: ${output.length.toLocaleString()} → ${maxChars.toLocaleString()} chars]`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AuditStateManager } from "../../managers/types"
|
|
2
|
+
import { createLogger } from "../../shared/logger"
|
|
3
|
+
|
|
4
|
+
export function createSessionRecoveryHandler(
|
|
5
|
+
auditStateManager: AuditStateManager,
|
|
6
|
+
) {
|
|
7
|
+
const logger = createLogger()
|
|
8
|
+
|
|
9
|
+
return async (event: { type: string; sessionId?: string }): Promise<void> => {
|
|
10
|
+
if (event.type !== "session.error") return
|
|
11
|
+
|
|
12
|
+
logger.info("Session error detected, attempting state recovery...")
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const recovered = await auditStateManager.load()
|
|
16
|
+
if (recovered) {
|
|
17
|
+
logger.info(
|
|
18
|
+
`State recovered: phase=${recovered.currentPhase}, findings=${recovered.findings.length}`,
|
|
19
|
+
)
|
|
20
|
+
} else {
|
|
21
|
+
logger.warn("No persisted state available for recovery")
|
|
22
|
+
}
|
|
23
|
+
} catch (error) {
|
|
24
|
+
logger.error("State recovery failed:", error)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createLogger } from "../../shared/logger"
|
|
2
|
+
|
|
3
|
+
const RECOVERY_HINTS: Record<string, string> = {
|
|
4
|
+
slither: "Install Slither: pip install slither-analyzer",
|
|
5
|
+
forge: "Install Foundry: curl -L https://foundry.paradigm.xyz | bash && foundryup",
|
|
6
|
+
solodit: "Check network connectivity or Solodit API status",
|
|
7
|
+
scvd: "Check SCVD API at https://api.scvd.dev — may be temporarily unavailable",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createToolErrorRecoveryHandler() {
|
|
11
|
+
const logger = createLogger()
|
|
12
|
+
|
|
13
|
+
return (toolResult: { tool: string; result: string }): string | null => {
|
|
14
|
+
const { tool, result } = toolResult
|
|
15
|
+
const lowerResult = result.toLowerCase()
|
|
16
|
+
|
|
17
|
+
const isError =
|
|
18
|
+
lowerResult.includes("enoent") ||
|
|
19
|
+
lowerResult.includes("not found") ||
|
|
20
|
+
lowerResult.includes("command failed") ||
|
|
21
|
+
lowerResult.includes("error:")
|
|
22
|
+
|
|
23
|
+
if (!isError) return null
|
|
24
|
+
|
|
25
|
+
const toolBase = tool.replace("argus_", "").split("_")[0] ?? ""
|
|
26
|
+
const hint = RECOVERY_HINTS[toolBase]
|
|
27
|
+
|
|
28
|
+
if (hint) {
|
|
29
|
+
logger.info(`Tool error recovery hint for ${tool}: ${hint}`)
|
|
30
|
+
return `\n[Argus Recovery Hint] ${hint}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { mkdir, rename } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import type { AuditStateManager } from "../../managers/types";
|
|
4
|
+
import { createAuditState } from "../../state/audit-state";
|
|
5
|
+
import type { AuditState, PersistentAuditState } from "../../state/types";
|
|
6
|
+
import { createLogger } from "../../shared/logger";
|
|
7
|
+
|
|
8
|
+
const STATE_FILE_DIR = ".opencode";
|
|
9
|
+
const STATE_FILE_NAME = "argus-state.json";
|
|
10
|
+
const STATE_VERSION = "1";
|
|
11
|
+
|
|
12
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
13
|
+
return typeof value === "object" && value !== null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isStringArray(value: unknown): value is string[] {
|
|
17
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isAuditState(value: unknown): value is AuditState {
|
|
21
|
+
if (!isObject(value)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
typeof value.sessionId === "string" &&
|
|
27
|
+
typeof value.projectDir === "string" &&
|
|
28
|
+
isStringArray(value.contractsReviewed) &&
|
|
29
|
+
Array.isArray(value.findings) &&
|
|
30
|
+
Array.isArray(value.toolsExecuted) &&
|
|
31
|
+
typeof value.currentPhase === "string" &&
|
|
32
|
+
isStringArray(value.scope) &&
|
|
33
|
+
typeof value.startTime === "number"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isPersistentAuditState(value: unknown): value is PersistentAuditState {
|
|
38
|
+
if (!isAuditState(value) || !isObject(value)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
typeof value.savedAt === "number" &&
|
|
44
|
+
typeof value.version === "string" &&
|
|
45
|
+
typeof value.filePath === "string"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createAuditStateManager(projectDir: string): AuditStateManager {
|
|
50
|
+
const logger = createLogger();
|
|
51
|
+
const stateFilePath = join(projectDir, STATE_FILE_DIR, STATE_FILE_NAME);
|
|
52
|
+
let currentState: AuditState = createAuditState(projectDir).state;
|
|
53
|
+
|
|
54
|
+
async function load(): Promise<AuditState | null> {
|
|
55
|
+
try {
|
|
56
|
+
const file = Bun.file(stateFilePath);
|
|
57
|
+
if (!(await file.exists())) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const content = await file.text();
|
|
62
|
+
if (!content.trim()) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parsed: unknown = JSON.parse(content);
|
|
67
|
+
if (!isPersistentAuditState(parsed)) {
|
|
68
|
+
logger.warn("Persistent audit state is invalid, ignoring", stateFilePath);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { savedAt: _savedAt, version: _version, filePath: _filePath, ...state } = parsed;
|
|
73
|
+
currentState = state;
|
|
74
|
+
return currentState;
|
|
75
|
+
} catch (_error) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function save(state: AuditState): Promise<void> {
|
|
81
|
+
currentState = state;
|
|
82
|
+
|
|
83
|
+
const persistentState: PersistentAuditState = {
|
|
84
|
+
...state,
|
|
85
|
+
savedAt: Date.now(),
|
|
86
|
+
version: STATE_VERSION,
|
|
87
|
+
filePath: stateFilePath,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const tempFilePath = `${stateFilePath}.tmp`;
|
|
91
|
+
await mkdir(dirname(stateFilePath), { recursive: true });
|
|
92
|
+
await Bun.write(tempFilePath, `${JSON.stringify(persistentState, null, 2)}\n`);
|
|
93
|
+
await rename(tempFilePath, stateFilePath);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function get(): AuditState {
|
|
97
|
+
return currentState;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function update(patch: Partial<AuditState>): Promise<void> {
|
|
101
|
+
currentState = {
|
|
102
|
+
...currentState,
|
|
103
|
+
...patch,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await save(currentState);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function reset(): Promise<void> {
|
|
110
|
+
currentState = createAuditState(projectDir).state;
|
|
111
|
+
await save(currentState);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
load,
|
|
116
|
+
save,
|
|
117
|
+
get,
|
|
118
|
+
update,
|
|
119
|
+
reset,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createAuditStateManager } from "./audit-state-manager";
|