solidity-argus 0.3.7 → 0.5.7
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 +24 -2
- package/src/agents/scribe-prompt.ts +34 -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/cli/commands/install.ts +74 -33
- 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 +806 -173
- 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 +68 -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 +602 -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 +130 -25
- package/src/tools/report-generator-tool.ts +475 -327
- 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/state/projectors.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto"
|
|
2
|
+
import { isRecord } from "../shared/type-guards"
|
|
3
|
+
import { SEVERITY_RANK } from "../shared/validation-constants"
|
|
2
4
|
import {
|
|
3
5
|
type AuditEvent,
|
|
4
6
|
type CanonicalFinding,
|
|
@@ -32,36 +34,39 @@ export class ProjectorError extends Error {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
export const SEVERITY_RANK: Record<CanonicalFinding["severity"], number> = {
|
|
36
|
-
Critical: 0,
|
|
37
|
-
High: 1,
|
|
38
|
-
Medium: 2,
|
|
39
|
-
Low: 3,
|
|
40
|
-
Informational: 4,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
44
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
37
|
function extractScope(payload: unknown): string[] {
|
|
48
38
|
if (!isRecord(payload) || !Array.isArray(payload.scope)) return []
|
|
49
39
|
return payload.scope.filter((entry): entry is string => typeof entry === "string")
|
|
50
40
|
}
|
|
51
41
|
|
|
42
|
+
const VALID_PHASES = new Set<string>([
|
|
43
|
+
"reconnaissance",
|
|
44
|
+
"scanning",
|
|
45
|
+
"manual-review",
|
|
46
|
+
"attack-surface",
|
|
47
|
+
"research",
|
|
48
|
+
"testing",
|
|
49
|
+
"reporting",
|
|
50
|
+
"complete",
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
function isAuditPhase(value: string): value is AuditPhase {
|
|
54
|
+
return VALID_PHASES.has(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
function extractPhase(payload: unknown): AuditPhase | undefined {
|
|
53
58
|
if (typeof payload === "string") {
|
|
54
|
-
return payload
|
|
59
|
+
return isAuditPhase(payload) ? payload : undefined
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
if (!isRecord(payload)) return undefined
|
|
58
63
|
|
|
59
|
-
if (typeof payload.phase === "string") {
|
|
60
|
-
return payload.phase
|
|
64
|
+
if (typeof payload.phase === "string" && isAuditPhase(payload.phase)) {
|
|
65
|
+
return payload.phase
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
if (typeof payload.currentPhase === "string") {
|
|
64
|
-
return payload.currentPhase
|
|
68
|
+
if (typeof payload.currentPhase === "string" && isAuditPhase(payload.currentPhase)) {
|
|
69
|
+
return payload.currentPhase
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
return undefined
|
|
@@ -86,7 +91,8 @@ function resolveToolName(event: AuditEvent, payload: Record<string, unknown>): s
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
function resolveFindingsCount(payload: Record<string, unknown>): number {
|
|
89
|
-
|
|
94
|
+
const count = typeof payload.findingsCount === "number" ? payload.findingsCount : 0
|
|
95
|
+
return Number.isFinite(count) ? Math.max(0, count) : 0
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
function resolveToolSuccess(payload: Record<string, unknown>): boolean {
|
|
@@ -406,12 +412,13 @@ export function projectReportInput(
|
|
|
406
412
|
const proxyContracts = extractLatestFromPayload(events, "proxyContracts", asProxyContracts)
|
|
407
413
|
const patternVersion = extractLatestFromPayload(events, "patternVersion", asString)
|
|
408
414
|
const skillsLoaded = extractLatestFromPayload(events, "skillsLoaded", asStringArray)
|
|
415
|
+
const unavailableTools = extractLatestFromPayload(events, "unavailableTools", asStringArray)
|
|
409
416
|
|
|
410
417
|
return {
|
|
411
418
|
run_id: runId,
|
|
412
419
|
seq: events.at(-1)?.seq ?? 0,
|
|
413
420
|
session_id: sessionCreated?.session_id ?? events[0]?.session_id ?? "",
|
|
414
|
-
tool_call_id: latestFinalized?.tool_call_id ?? "",
|
|
421
|
+
tool_call_id: latestFinalized?.tool_call_id ?? "pending-finalization",
|
|
415
422
|
source: latestFinalized?.source ?? events[0]?.source ?? "projector",
|
|
416
423
|
schema_version: latestFinalized?.schema_version ?? events[0]?.schema_version ?? SCHEMA_VERSION,
|
|
417
424
|
projectDir,
|
|
@@ -425,6 +432,7 @@ export function projectReportInput(
|
|
|
425
432
|
proxyContracts,
|
|
426
433
|
patternVersion,
|
|
427
434
|
skillsLoaded,
|
|
435
|
+
unavailableTools,
|
|
428
436
|
}
|
|
429
437
|
}
|
|
430
438
|
|
package/src/state/schemas.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { isRecord } from "../shared/type-guards"
|
|
2
|
+
import {
|
|
3
|
+
VALID_AGENTS,
|
|
4
|
+
VALID_CONFIDENCES,
|
|
5
|
+
VALID_SEVERITIES,
|
|
6
|
+
VALID_SOURCES,
|
|
7
|
+
} from "../shared/validation-constants"
|
|
1
8
|
import type {
|
|
2
9
|
ArgusAgentName,
|
|
3
10
|
AuditPhase,
|
|
@@ -112,6 +119,7 @@ export interface ReportInput {
|
|
|
112
119
|
proxyContracts?: ProxyContract[]
|
|
113
120
|
patternVersion?: string
|
|
114
121
|
skillsLoaded?: string[]
|
|
122
|
+
unavailableTools?: string[]
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
function pushRequiredRootStringError(
|
|
@@ -142,38 +150,6 @@ function pushRequiredRootNumberError(
|
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
const VALID_SEVERITIES: ReadonlySet<FindingSeverity> = new Set([
|
|
146
|
-
"Critical",
|
|
147
|
-
"High",
|
|
148
|
-
"Medium",
|
|
149
|
-
"Low",
|
|
150
|
-
"Informational",
|
|
151
|
-
])
|
|
152
|
-
const VALID_CONFIDENCES: ReadonlySet<CanonicalFinding["confidence"]> = new Set([
|
|
153
|
-
"High",
|
|
154
|
-
"Medium",
|
|
155
|
-
"Low",
|
|
156
|
-
])
|
|
157
|
-
const VALID_SOURCES: ReadonlySet<CanonicalFinding["source"]> = new Set([
|
|
158
|
-
"slither",
|
|
159
|
-
"manual",
|
|
160
|
-
"pattern",
|
|
161
|
-
"scvd",
|
|
162
|
-
"solodit",
|
|
163
|
-
"fuzz",
|
|
164
|
-
])
|
|
165
|
-
const VALID_AGENTS: ReadonlySet<ArgusAgentName> = new Set([
|
|
166
|
-
"argus",
|
|
167
|
-
"sentinel",
|
|
168
|
-
"pythia",
|
|
169
|
-
"scribe",
|
|
170
|
-
"unknown",
|
|
171
|
-
])
|
|
172
|
-
|
|
173
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
174
|
-
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
153
|
function pushRequiredStringError(
|
|
178
154
|
errors: ValidationError[],
|
|
179
155
|
obj: Record<string, unknown>,
|
package/src/state/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import { basename } from "node:path"
|
|
3
3
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
4
|
+
import { FOUNDRY_NOT_FOUND_MESSAGE } from "../shared/forge-errors"
|
|
4
5
|
import { findFoundryProjectDir } from "../shared/project-utils"
|
|
5
6
|
import type { ContractProfile } from "../state/types"
|
|
6
7
|
import { extractContractInfo, parseExternalCalls } from "../utils/solidity-parser"
|
|
@@ -39,7 +40,8 @@ function createFailureProfile(
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function addIndicator(indicators: Set<string>, source: string, indicator: string): void {
|
|
42
|
-
|
|
43
|
+
const keyword = indicator.split("uses-")[1]
|
|
44
|
+
if (keyword && source.includes(keyword)) {
|
|
43
45
|
indicators.add(indicator)
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -224,11 +226,7 @@ export async function executeContractAnalyzer(
|
|
|
224
226
|
|
|
225
227
|
const maybeError = error as Error & { code?: string }
|
|
226
228
|
if (maybeError.code === "ENOENT") {
|
|
227
|
-
return createFailureProfile(
|
|
228
|
-
contractName,
|
|
229
|
-
filePath,
|
|
230
|
-
"Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash",
|
|
231
|
-
)
|
|
229
|
+
return createFailureProfile(contractName, filePath, FOUNDRY_NOT_FOUND_MESSAGE)
|
|
232
230
|
}
|
|
233
231
|
|
|
234
232
|
const message = maybeError.message || "contract analysis failed"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { classifyForgeError } from "../shared/forge-errors"
|
|
3
|
+
import { runForgeCommand } from "../shared/forge-runner"
|
|
2
4
|
import { resolveProjectDir } from "../shared/project-utils"
|
|
3
5
|
|
|
4
6
|
type ForgeCoverageArgs = {
|
|
@@ -38,8 +40,7 @@ type ForgeCoverageResult = {
|
|
|
38
40
|
|
|
39
41
|
export type ForgeCommandRunner = (
|
|
40
42
|
command: string[],
|
|
41
|
-
|
|
42
|
-
cwd: string,
|
|
43
|
+
options: { signal?: AbortSignal; cwd?: string; env?: Record<string, string> },
|
|
43
44
|
) => Promise<{ stdout: string; stderr: string; exitCode: number }>
|
|
44
45
|
|
|
45
46
|
const EMPTY_SUMMARY: ForgeCoverageSummary = {
|
|
@@ -138,27 +139,6 @@ function parseCoverageReport(output: string): ForgeCoverageReport {
|
|
|
138
139
|
return { files, summary }
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
const runForgeCommand: ForgeCommandRunner = async (command, signal, cwd) => {
|
|
142
|
-
const child = Bun.spawn(command, {
|
|
143
|
-
cwd,
|
|
144
|
-
stdout: "pipe",
|
|
145
|
-
stderr: "pipe",
|
|
146
|
-
signal,
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
150
|
-
child.exited,
|
|
151
|
-
new Response(child.stdout).text(),
|
|
152
|
-
new Response(child.stderr).text(),
|
|
153
|
-
])
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
stdout,
|
|
157
|
-
stderr,
|
|
158
|
-
exitCode,
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
142
|
export async function executeForgeCoverage(
|
|
163
143
|
args: ForgeCoverageArgs,
|
|
164
144
|
context: ToolContext,
|
|
@@ -176,7 +156,10 @@ export async function executeForgeCoverage(
|
|
|
176
156
|
})
|
|
177
157
|
|
|
178
158
|
try {
|
|
179
|
-
const runResult = await runCommand(["forge", "coverage"],
|
|
159
|
+
const runResult = await runCommand(["forge", "coverage"], {
|
|
160
|
+
signal: context.abort,
|
|
161
|
+
cwd: normalizedArgs.target,
|
|
162
|
+
})
|
|
180
163
|
|
|
181
164
|
if (runResult.exitCode !== 0) {
|
|
182
165
|
return fail(
|
|
@@ -197,18 +180,10 @@ export async function executeForgeCoverage(
|
|
|
197
180
|
executionTime: Date.now() - startedAt,
|
|
198
181
|
}
|
|
199
182
|
} catch (error) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const maybeError = error as Error & { code?: string }
|
|
205
|
-
if (maybeError.code === "ENOENT") {
|
|
206
|
-
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
207
|
-
}
|
|
208
|
-
if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
|
|
209
|
-
return fail("forge coverage timed out")
|
|
210
|
-
}
|
|
183
|
+
const classified = classifyForgeError(error, context, "forge coverage")
|
|
184
|
+
if (classified) return fail(classified)
|
|
211
185
|
|
|
186
|
+
const maybeError = error as Error
|
|
212
187
|
return fail(maybeError.message || "forge coverage failed")
|
|
213
188
|
}
|
|
214
189
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { classifyForgeError } from "../shared/forge-errors"
|
|
3
|
+
import { runForgeCommand } from "../shared/forge-runner"
|
|
4
|
+
import { assertContained, validateUrlScheme } from "../shared/path-containment"
|
|
2
5
|
import { resolveProjectDir } from "../shared/project-utils"
|
|
3
6
|
|
|
4
7
|
type ForgeFuzzArgs = {
|
|
@@ -47,16 +50,20 @@ export type ForgeFuzzCommandResult = {
|
|
|
47
50
|
|
|
48
51
|
type RunForgeFuzzCommand = (
|
|
49
52
|
command: string[],
|
|
50
|
-
|
|
51
|
-
cwd: string,
|
|
52
|
-
env: Record<string, string>,
|
|
53
|
+
options: { signal?: AbortSignal; cwd?: string; env?: Record<string, string> },
|
|
53
54
|
) => Promise<ForgeFuzzCommandResult>
|
|
54
55
|
|
|
55
56
|
function normalizeArgs(args: ForgeFuzzArgs, context: ToolContext): NormalizedForgeFuzzArgs {
|
|
56
57
|
const requestedRuns =
|
|
57
58
|
typeof args.runs === "number" && Number.isFinite(args.runs) ? args.runs : 256
|
|
58
59
|
const clampedRuns = Math.max(1, Math.min(10000, Math.floor(requestedRuns)))
|
|
59
|
-
const
|
|
60
|
+
const projectRoot = resolveProjectDir(context)
|
|
61
|
+
const target =
|
|
62
|
+
args.target && args.target !== "." ? assertContained(args.target, projectRoot) : projectRoot
|
|
63
|
+
|
|
64
|
+
if (args.fork_url && !validateUrlScheme(args.fork_url)) {
|
|
65
|
+
throw new Error(`fork_url must use http:// or https:// scheme, got: "${args.fork_url}"`)
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
return {
|
|
62
69
|
target,
|
|
@@ -177,36 +184,12 @@ function parseCounterexampleLine(line: string):
|
|
|
177
184
|
}
|
|
178
185
|
}
|
|
179
186
|
|
|
180
|
-
const runForgeFuzzCommand: RunForgeFuzzCommand = async (command, signal, cwd, env) => {
|
|
181
|
-
const child = Bun.spawn(command, {
|
|
182
|
-
cwd,
|
|
183
|
-
stdout: "pipe",
|
|
184
|
-
stderr: "pipe",
|
|
185
|
-
signal,
|
|
186
|
-
env,
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
190
|
-
child.exited,
|
|
191
|
-
new Response(child.stdout).text(),
|
|
192
|
-
new Response(child.stderr).text(),
|
|
193
|
-
])
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
stdout,
|
|
197
|
-
stderr,
|
|
198
|
-
exitCode,
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
187
|
export async function executeForgeFuzz(
|
|
203
188
|
args: ForgeFuzzArgs,
|
|
204
189
|
context: ToolContext,
|
|
205
|
-
runCommand: RunForgeFuzzCommand =
|
|
190
|
+
runCommand: RunForgeFuzzCommand = runForgeCommand,
|
|
206
191
|
): Promise<ForgeFuzzResult> {
|
|
207
192
|
const startedAt = Date.now()
|
|
208
|
-
const normalized = normalizeArgs(args, context)
|
|
209
|
-
context.metadata({ title: `Run forge fuzz: ${normalized.target}` })
|
|
210
193
|
|
|
211
194
|
const fail = (error: string): ForgeFuzzResult => ({
|
|
212
195
|
success: false,
|
|
@@ -218,17 +201,12 @@ export async function executeForgeFuzz(
|
|
|
218
201
|
})
|
|
219
202
|
|
|
220
203
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
buildForgeFuzzCommand(normalized),
|
|
228
|
-
context.abort,
|
|
229
|
-
normalized.target,
|
|
230
|
-
env,
|
|
231
|
-
)
|
|
204
|
+
const normalized = normalizeArgs(args, context)
|
|
205
|
+
context.metadata({ title: `Run forge fuzz: ${normalized.target}` })
|
|
206
|
+
const runResult = await runCommand(buildForgeFuzzCommand(normalized), {
|
|
207
|
+
signal: context.abort,
|
|
208
|
+
cwd: normalized.target,
|
|
209
|
+
})
|
|
232
210
|
|
|
233
211
|
const lines = `${runResult.stdout}\n${runResult.stderr}`
|
|
234
212
|
.split(/\r?\n/)
|
|
@@ -278,18 +256,10 @@ export async function executeForgeFuzz(
|
|
|
278
256
|
|
|
279
257
|
return output
|
|
280
258
|
} catch (error) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const maybeError = error as Error & { code?: string }
|
|
286
|
-
if (maybeError.code === "ENOENT") {
|
|
287
|
-
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
288
|
-
}
|
|
289
|
-
if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
|
|
290
|
-
return fail("forge fuzz timed out")
|
|
291
|
-
}
|
|
259
|
+
const classified = classifyForgeError(error, context, "forge fuzz")
|
|
260
|
+
if (classified) return fail(classified)
|
|
292
261
|
|
|
262
|
+
const maybeError = error as Error
|
|
293
263
|
return fail(maybeError.message || "forge fuzz failed")
|
|
294
264
|
}
|
|
295
265
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { classifyForgeError } from "../shared/forge-errors"
|
|
3
|
+
import { runForgeCommand } from "../shared/forge-runner"
|
|
4
|
+
import { assertContained, validateUrlScheme } from "../shared/path-containment"
|
|
2
5
|
import { resolveProjectDir } from "../shared/project-utils"
|
|
3
6
|
import { extractJson } from "../utils/solidity-parser"
|
|
4
7
|
|
|
@@ -62,8 +65,7 @@ export type ForgeCommandResult = {
|
|
|
62
65
|
|
|
63
66
|
type RunForgeCommand = (
|
|
64
67
|
command: string[],
|
|
65
|
-
|
|
66
|
-
cwd: string,
|
|
68
|
+
options: { signal?: AbortSignal; cwd?: string; env?: Record<string, string> },
|
|
67
69
|
) => Promise<ForgeCommandResult>
|
|
68
70
|
|
|
69
71
|
type ForgeTestPayload = {
|
|
@@ -277,7 +279,14 @@ function parseCoverage(payload: CoveragePayload): { files: ForgeCoverageFile[] }
|
|
|
277
279
|
}
|
|
278
280
|
|
|
279
281
|
function normalizeArgs(args: ForgeTestArgs, context: ToolContext): NormalizedForgeTestArgs {
|
|
280
|
-
const
|
|
282
|
+
const projectRoot = resolveProjectDir(context)
|
|
283
|
+
const target =
|
|
284
|
+
args.target && args.target !== "." ? assertContained(args.target, projectRoot) : projectRoot
|
|
285
|
+
|
|
286
|
+
if (args.fork_url && !validateUrlScheme(args.fork_url)) {
|
|
287
|
+
throw new Error(`fork_url must use http:// or https:// scheme, got: "${args.fork_url}"`)
|
|
288
|
+
}
|
|
289
|
+
|
|
281
290
|
return {
|
|
282
291
|
target,
|
|
283
292
|
match_test: args.match_test,
|
|
@@ -311,35 +320,12 @@ function buildForgeTestCommand(args: NormalizedForgeTestArgs): string[] {
|
|
|
311
320
|
return command
|
|
312
321
|
}
|
|
313
322
|
|
|
314
|
-
const runForgeCommand: RunForgeCommand = async (command, signal, cwd) => {
|
|
315
|
-
const child = Bun.spawn(command, {
|
|
316
|
-
cwd,
|
|
317
|
-
stdout: "pipe",
|
|
318
|
-
stderr: "pipe",
|
|
319
|
-
signal,
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
323
|
-
child.exited,
|
|
324
|
-
new Response(child.stdout).text(),
|
|
325
|
-
new Response(child.stderr).text(),
|
|
326
|
-
])
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
stdout,
|
|
330
|
-
stderr,
|
|
331
|
-
exitCode,
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
323
|
export async function executeForgeTest(
|
|
336
324
|
args: ForgeTestArgs,
|
|
337
325
|
context: ToolContext,
|
|
338
326
|
runCommand: RunForgeCommand = runForgeCommand,
|
|
339
327
|
): Promise<ForgeTestResult> {
|
|
340
328
|
const startedAt = Date.now()
|
|
341
|
-
const normalizedArgs = normalizeArgs(args, context)
|
|
342
|
-
context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
|
|
343
329
|
|
|
344
330
|
const fail = (error: string): ForgeTestResult => ({
|
|
345
331
|
success: false,
|
|
@@ -350,11 +336,12 @@ export async function executeForgeTest(
|
|
|
350
336
|
})
|
|
351
337
|
|
|
352
338
|
try {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
339
|
+
const normalizedArgs = normalizeArgs(args, context)
|
|
340
|
+
context.metadata({ title: `Run forge test: ${normalizedArgs.target}` })
|
|
341
|
+
const testResult = await runCommand(buildForgeTestCommand(normalizedArgs), {
|
|
342
|
+
signal: context.abort,
|
|
343
|
+
cwd: normalizedArgs.target,
|
|
344
|
+
})
|
|
358
345
|
|
|
359
346
|
let payload: ForgeTestPayload
|
|
360
347
|
try {
|
|
@@ -380,11 +367,10 @@ export async function executeForgeTest(
|
|
|
380
367
|
}
|
|
381
368
|
|
|
382
369
|
if (normalizedArgs.coverage) {
|
|
383
|
-
const coverageResult = await runCommand(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
)
|
|
370
|
+
const coverageResult = await runCommand(["forge", "coverage", "--report", "json"], {
|
|
371
|
+
signal: context.abort,
|
|
372
|
+
cwd: normalizedArgs.target,
|
|
373
|
+
})
|
|
388
374
|
if (coverageResult.exitCode !== 0) {
|
|
389
375
|
output.error = coverageResult.stderr.trim() || "forge coverage failed"
|
|
390
376
|
output.success = false
|
|
@@ -406,18 +392,10 @@ export async function executeForgeTest(
|
|
|
406
392
|
|
|
407
393
|
return output
|
|
408
394
|
} catch (error) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const maybeError = error as Error & { code?: string }
|
|
414
|
-
if (maybeError.code === "ENOENT") {
|
|
415
|
-
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
416
|
-
}
|
|
417
|
-
if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
|
|
418
|
-
return fail("forge test timed out")
|
|
419
|
-
}
|
|
395
|
+
const classified = classifyForgeError(error, context, "forge test")
|
|
396
|
+
if (classified) return fail(classified)
|
|
420
397
|
|
|
398
|
+
const maybeError = error as Error
|
|
421
399
|
return fail(maybeError.message || "forge test failed")
|
|
422
400
|
}
|
|
423
401
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { classifyForgeError } from "../shared/forge-errors"
|
|
3
|
+
import { runForgeCommand } from "../shared/forge-runner"
|
|
2
4
|
import { resolveProjectDir } from "../shared/project-utils"
|
|
3
5
|
|
|
4
6
|
type GasAnalysisArgs = {
|
|
@@ -43,8 +45,7 @@ type GasAnalysisResult = {
|
|
|
43
45
|
|
|
44
46
|
export type ForgeCommandRunner = (
|
|
45
47
|
command: string[],
|
|
46
|
-
|
|
47
|
-
cwd: string,
|
|
48
|
+
options: { signal?: AbortSignal; cwd?: string; env?: Record<string, string> },
|
|
48
49
|
) => Promise<{ stdout: string; stderr: string; exitCode: number }>
|
|
49
50
|
|
|
50
51
|
function toNumber(value: string): number {
|
|
@@ -58,7 +59,7 @@ function toNumber(value: string): number {
|
|
|
58
59
|
|
|
59
60
|
function parseCells(line: string): string[] {
|
|
60
61
|
return line
|
|
61
|
-
.split(/[
|
|
62
|
+
.split(/[│┆|]/)
|
|
62
63
|
.map((cell) => cell.trim())
|
|
63
64
|
.filter((cell) => cell.length > 0)
|
|
64
65
|
}
|
|
@@ -75,7 +76,7 @@ function parseGasReport(stdout: string): ContractGasReport[] {
|
|
|
75
76
|
let inFunctionSection = false
|
|
76
77
|
|
|
77
78
|
for (const line of lines) {
|
|
78
|
-
if (!line.includes("│") && !line.includes("┆")) {
|
|
79
|
+
if (!line.includes("│") && !line.includes("┆") && !line.includes("|")) {
|
|
79
80
|
continue
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -161,27 +162,6 @@ function normalizeArgs(args: GasAnalysisArgs, context: ToolContext): NormalizedG
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
const runForgeCommand: ForgeCommandRunner = async (command, signal, cwd) => {
|
|
165
|
-
const child = Bun.spawn(command, {
|
|
166
|
-
cwd,
|
|
167
|
-
stdout: "pipe",
|
|
168
|
-
stderr: "pipe",
|
|
169
|
-
signal,
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
173
|
-
child.exited,
|
|
174
|
-
new Response(child.stdout).text(),
|
|
175
|
-
new Response(child.stderr).text(),
|
|
176
|
-
])
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
stdout,
|
|
180
|
-
stderr,
|
|
181
|
-
exitCode,
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
165
|
export async function executeGasAnalysis(
|
|
186
166
|
args: GasAnalysisArgs,
|
|
187
167
|
context: ToolContext,
|
|
@@ -200,11 +180,10 @@ export async function executeGasAnalysis(
|
|
|
200
180
|
})
|
|
201
181
|
|
|
202
182
|
try {
|
|
203
|
-
const forgeResult = await runCommand(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
183
|
+
const forgeResult = await runCommand(["forge", "test", "--gas-report"], {
|
|
184
|
+
signal: context.abort,
|
|
185
|
+
cwd: normalizedArgs.target,
|
|
186
|
+
})
|
|
208
187
|
|
|
209
188
|
const contracts = parseGasReport(forgeResult.stdout)
|
|
210
189
|
const hotspots = contracts
|
|
@@ -234,18 +213,10 @@ export async function executeGasAnalysis(
|
|
|
234
213
|
|
|
235
214
|
return output
|
|
236
215
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const maybeError = error as Error & { code?: string }
|
|
242
|
-
if (maybeError.code === "ENOENT") {
|
|
243
|
-
return fail("Foundry not found. Install: curl -L https://foundry.paradigm.xyz | bash")
|
|
244
|
-
}
|
|
245
|
-
if (maybeError.code === "ETIMEDOUT" || maybeError.message.toLowerCase().includes("timed out")) {
|
|
246
|
-
return fail("forge gas analysis timed out")
|
|
247
|
-
}
|
|
216
|
+
const classified = classifyForgeError(error, context, "forge gas analysis")
|
|
217
|
+
if (classified) return fail(classified)
|
|
248
218
|
|
|
219
|
+
const maybeError = error as Error
|
|
249
220
|
return fail(maybeError.message || "forge gas analysis failed")
|
|
250
221
|
}
|
|
251
222
|
}
|