athena-mcp 1.0.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 +477 -0
- package/install.js +327 -0
- package/mcp/servers.json +100 -0
- package/mcp/tools/README.md +64 -0
- package/mcp/tools/__init__.py +1 -0
- package/mcp/tools/aderyn_runner.py +226 -0
- package/mcp/tools/eas_attest.py +404 -0
- package/mcp/tools/evidence_chain.py +363 -0
- package/mcp/tools/exploit_simulator.py +545 -0
- package/mcp/tools/fuzz_runner.py +440 -0
- package/mcp/tools/gev_analyzer.py +362 -0
- package/mcp/tools/halmos_runner.py +408 -0
- package/mcp/tools/incremental_auditor.py +441 -0
- package/mcp/tools/knowledge_base.py +378 -0
- package/mcp/tools/poc_generator.py +479 -0
- package/mcp/tools/protocol_scanner.py +456 -0
- package/mcp/tools/repair_validator.py +421 -0
- package/mcp/tools/slither_runner.py +221 -0
- package/package.json +52 -0
- package/requirements.txt +20 -0
- package/skills/glm-audit-skill/SKILL.md +73 -0
- package/skills/glm-audit-skill/references/audit-agents/access-control-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/asymmetry-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/boundary-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/economic-security-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/execution-trace-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/first-principles-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/flow-gap-agent.md +38 -0
- package/skills/glm-audit-skill/references/audit-agents/invariant-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/math-precision-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/numerical-gap-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/periphery-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/shared-rules.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/trust-gap-agent.md +39 -0
- package/skills/glm-audit-skill/references/judging.md +45 -0
- package/skills/glm-audit-skill/references/report-formatting.md +22 -0
- package/skills/glm-audit-skill/references/senior-auditor-sop.md +34 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server: Halmos Runner
|
|
4
|
+
Symbolic verification tool using Halmos (a16z) for Solidity contracts.
|
|
5
|
+
Verifies contract properties using Z3 SMT solver.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import asyncio
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
17
|
+
logger = logging.getLogger("halmos_runner")
|
|
18
|
+
|
|
19
|
+
TOOL_NAME = "halmos_runner"
|
|
20
|
+
TOOL_VERSION = "1.0.0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_tool_definitions() -> list:
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
"name": "halmos_verify",
|
|
27
|
+
"description": "Run symbolic verification on Solidity contracts using Halmos. Verifies contract properties like balance invariants, access control, etc.",
|
|
28
|
+
"inputSchema": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"properties": {
|
|
31
|
+
"contract_path": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "Path to Solidity contract file"
|
|
34
|
+
},
|
|
35
|
+
"properties": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {"type": "string"},
|
|
38
|
+
"description": "Properties to verify (e.g., ['balance >= 0', 'totalSupply == sum(balances)'])"
|
|
39
|
+
},
|
|
40
|
+
"contract_name": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Specific contract name to verify (optional, verifies all if omitted)"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": ["contract_path"]
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "halmos_check_invariants",
|
|
50
|
+
"description": "Check common security invariants for a contract (balance >= 0, no overflow, access control)",
|
|
51
|
+
"inputSchema": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"contract_path": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Path to Solidity contract file"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"required": ["contract_path"]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "halmos_generate_tests",
|
|
64
|
+
"description": "Generate Halmos test stubs for contract properties",
|
|
65
|
+
"inputSchema": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"contract_path": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Path to Solidity contract file"
|
|
71
|
+
},
|
|
72
|
+
"properties": {
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {"type": "string"},
|
|
75
|
+
"description": "Properties to generate tests for"
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"required": ["contract_path"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def halmos_verify(contract_path: str, properties: list = None, contract_name: str = None) -> dict:
|
|
85
|
+
"""Run Halmos symbolic verification."""
|
|
86
|
+
contract_path = os.path.abspath(contract_path)
|
|
87
|
+
|
|
88
|
+
if not os.path.isfile(contract_path):
|
|
89
|
+
return {"success": False, "error": f"Contract file not found: {contract_path}"}
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Check if halmos is installed
|
|
93
|
+
check_proc = await asyncio.create_subprocess_exec(
|
|
94
|
+
"halmos", "--version",
|
|
95
|
+
stdout=asyncio.subprocess.PIPE,
|
|
96
|
+
stderr=asyncio.subprocess.PIPE
|
|
97
|
+
)
|
|
98
|
+
await check_proc.communicate()
|
|
99
|
+
|
|
100
|
+
if check_proc.returncode != 0:
|
|
101
|
+
# Halmos not installed - return mock result
|
|
102
|
+
return {
|
|
103
|
+
"success": True,
|
|
104
|
+
"mock": True,
|
|
105
|
+
"message": "Halmos not installed. Install: pip install halmos",
|
|
106
|
+
"findings": _generate_mock_findings(contract_path, properties)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Build halmos command
|
|
110
|
+
cmd = ["halmos", "--contract", contract_path]
|
|
111
|
+
|
|
112
|
+
if contract_name:
|
|
113
|
+
cmd.extend(["--contract-name", contract_name])
|
|
114
|
+
|
|
115
|
+
# Run halmos
|
|
116
|
+
logger.info(f"Running halmos: {' '.join(cmd)}")
|
|
117
|
+
|
|
118
|
+
proc = await asyncio.create_subprocess_exec(
|
|
119
|
+
*cmd,
|
|
120
|
+
stdout=asyncio.subprocess.PIPE,
|
|
121
|
+
stderr=asyncio.subprocess.PIPE
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
|
|
125
|
+
|
|
126
|
+
stdout_text = stdout.decode("utf-8", errors="replace")
|
|
127
|
+
stderr_text = stderr.decode("utf-8", errors="replace")
|
|
128
|
+
|
|
129
|
+
# Parse halmos output
|
|
130
|
+
findings = _parse_halmos_output(stdout_text, stderr_text)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"success": True,
|
|
134
|
+
"exit_code": proc.returncode,
|
|
135
|
+
"findings": findings,
|
|
136
|
+
"properties_verified": len([f for f in findings if f["status"] == "verified"]),
|
|
137
|
+
"properties_failed": len([f for f in findings if f["status"] == "failed"]),
|
|
138
|
+
"raw_output": stdout_text[:2000]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
except asyncio.TimeoutError:
|
|
142
|
+
return {"success": False, "error": "Halmos verification timed out (120s)"}
|
|
143
|
+
except FileNotFoundError:
|
|
144
|
+
return {
|
|
145
|
+
"success": True,
|
|
146
|
+
"mock": True,
|
|
147
|
+
"message": "Halmos not installed. Install: pip install halmos",
|
|
148
|
+
"findings": _generate_mock_findings(contract_path, properties)
|
|
149
|
+
}
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.exception("Halmos verification failed")
|
|
152
|
+
return {"success": False, "error": str(e)}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _parse_halmos_output(stdout: str, stderr: str) -> list:
|
|
156
|
+
"""Parse halmos output to extract findings."""
|
|
157
|
+
findings = []
|
|
158
|
+
|
|
159
|
+
# Halmos output format varies, parse common patterns
|
|
160
|
+
lines = stdout.split("\n")
|
|
161
|
+
|
|
162
|
+
for line in lines:
|
|
163
|
+
line = line.strip()
|
|
164
|
+
|
|
165
|
+
# Look for verification results
|
|
166
|
+
if "PASS" in line or "VERIFIED" in line:
|
|
167
|
+
findings.append({
|
|
168
|
+
"property": _extract_property(line),
|
|
169
|
+
"status": "verified",
|
|
170
|
+
"message": line
|
|
171
|
+
})
|
|
172
|
+
elif "FAIL" in line or "COUNTEREXAMPLE" in line:
|
|
173
|
+
findings.append({
|
|
174
|
+
"property": _extract_property(line),
|
|
175
|
+
"status": "failed",
|
|
176
|
+
"message": line,
|
|
177
|
+
"counterexample": _extract_counterexample(line)
|
|
178
|
+
})
|
|
179
|
+
elif "UNKNOWN" in line or "TIMEOUT" in line:
|
|
180
|
+
findings.append({
|
|
181
|
+
"property": _extract_property(line),
|
|
182
|
+
"status": "unknown",
|
|
183
|
+
"message": line
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
# If no findings parsed, create generic result
|
|
187
|
+
if not findings:
|
|
188
|
+
findings.append({
|
|
189
|
+
"property": "general",
|
|
190
|
+
"status": "unknown",
|
|
191
|
+
"message": "Could not parse halmos output"
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return findings
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _extract_property(line: str) -> str:
|
|
198
|
+
"""Extract property name from halmos output line."""
|
|
199
|
+
# Try to extract function/property name
|
|
200
|
+
if "::" in line:
|
|
201
|
+
return line.split("::")[-1].split()[0]
|
|
202
|
+
return "unknown"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _extract_counterexample(line: str) -> str:
|
|
206
|
+
"""Extract counterexample from halmos output."""
|
|
207
|
+
if "counterexample" in line.lower():
|
|
208
|
+
return line
|
|
209
|
+
return ""
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _generate_mock_findings(contract_path: str, properties: list = None) -> list:
|
|
213
|
+
"""Generate mock findings when halmos is not installed."""
|
|
214
|
+
findings = []
|
|
215
|
+
|
|
216
|
+
# Common properties to check
|
|
217
|
+
default_properties = [
|
|
218
|
+
"balance >= 0",
|
|
219
|
+
"totalSupply == sum(balances)",
|
|
220
|
+
"only_owner_can_admin",
|
|
221
|
+
"no_overflow"
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
check_properties = properties or default_properties
|
|
225
|
+
|
|
226
|
+
for prop in check_properties:
|
|
227
|
+
findings.append({
|
|
228
|
+
"property": prop,
|
|
229
|
+
"status": "mock",
|
|
230
|
+
"message": f"Mock verification (halmos not installed): {prop}",
|
|
231
|
+
"recommendation": "Install halmos and run actual verification"
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return findings
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def halmos_check_invariants(contract_path: str) -> dict:
|
|
238
|
+
"""Check common security invariants."""
|
|
239
|
+
# Common invariants to verify
|
|
240
|
+
invariants = [
|
|
241
|
+
"balance >= 0",
|
|
242
|
+
"totalSupply >= 0",
|
|
243
|
+
"no_reentrancy",
|
|
244
|
+
"access_control_valid",
|
|
245
|
+
"no_overflow_underflow"
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
result = await halmos_verify(contract_path, invariants)
|
|
249
|
+
|
|
250
|
+
if result.get("success"):
|
|
251
|
+
result["invariants_checked"] = invariants
|
|
252
|
+
result["summary"] = f"Checked {len(invariants)} invariants"
|
|
253
|
+
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def halmos_generate_tests(contract_path: str, properties: list = None) -> dict:
|
|
258
|
+
"""Generate Halmos test stubs."""
|
|
259
|
+
contract_path = os.path.abspath(contract_path)
|
|
260
|
+
|
|
261
|
+
if not os.path.isfile(contract_path):
|
|
262
|
+
return {"success": False, "error": f"Contract file not found: {contract_path}"}
|
|
263
|
+
|
|
264
|
+
# Read contract to extract contract name
|
|
265
|
+
try:
|
|
266
|
+
with open(contract_path, 'r') as f:
|
|
267
|
+
content = f.read()
|
|
268
|
+
|
|
269
|
+
import re
|
|
270
|
+
match = re.search(r'contract\s+(\w+)', content)
|
|
271
|
+
contract_name = match.group(1) if match else "Target"
|
|
272
|
+
except Exception:
|
|
273
|
+
contract_name = "Target"
|
|
274
|
+
|
|
275
|
+
# Generate test stubs
|
|
276
|
+
default_properties = properties or [
|
|
277
|
+
"balance_never_negative",
|
|
278
|
+
"total_supply_conserved",
|
|
279
|
+
"only_owner_can_withdraw"
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
test_code = f'''// SPDX-License-Identifier: MIT
|
|
283
|
+
pragma solidity ^0.8.20;
|
|
284
|
+
|
|
285
|
+
import "forge-std/Test.sol";
|
|
286
|
+
import "{contract_path}";
|
|
287
|
+
|
|
288
|
+
/// @title HalmosTests - Generated symbolic verification tests
|
|
289
|
+
contract HalmosTests is Test {{
|
|
290
|
+
{contract_name} public target;
|
|
291
|
+
|
|
292
|
+
function setUp() public {{
|
|
293
|
+
target = new {contract_name}();
|
|
294
|
+
}}
|
|
295
|
+
|
|
296
|
+
'''
|
|
297
|
+
|
|
298
|
+
for i, prop in enumerate(default_properties):
|
|
299
|
+
test_code += f''' /// @notice Verify: {prop}
|
|
300
|
+
function check_{prop}() public {{
|
|
301
|
+
// TODO: Implement property verification
|
|
302
|
+
// Use symbolic variables for inputs
|
|
303
|
+
// Assert invariant holds for all possible inputs
|
|
304
|
+
}}
|
|
305
|
+
|
|
306
|
+
'''
|
|
307
|
+
|
|
308
|
+
test_code += ''' /// @notice Helper: Check balance invariant
|
|
309
|
+
function check_balance_non_negative() public view {
|
|
310
|
+
// Symbolic verification that balance >= 0
|
|
311
|
+
// This is always true for uint256, but worth documenting
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
'''
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"success": True,
|
|
318
|
+
"contract_name": contract_name,
|
|
319
|
+
"properties": default_properties,
|
|
320
|
+
"test_code": test_code,
|
|
321
|
+
"usage": "Save to test/HalmosTests.t.sol and run: forge test"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
async def execute_tool(tool_name: str, arguments: dict) -> dict:
|
|
326
|
+
if tool_name == "halmos_verify":
|
|
327
|
+
return await halmos_verify(
|
|
328
|
+
arguments.get("contract_path", ""),
|
|
329
|
+
arguments.get("properties"),
|
|
330
|
+
arguments.get("contract_name")
|
|
331
|
+
)
|
|
332
|
+
elif tool_name == "halmos_check_invariants":
|
|
333
|
+
return await halmos_check_invariants(arguments.get("contract_path", ""))
|
|
334
|
+
elif tool_name == "halmos_generate_tests":
|
|
335
|
+
return await halmos_generate_tests(
|
|
336
|
+
arguments.get("contract_path", ""),
|
|
337
|
+
arguments.get("properties")
|
|
338
|
+
)
|
|
339
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
async def handle_request(request: dict) -> dict:
|
|
343
|
+
method = request.get("method")
|
|
344
|
+
params = request.get("params", {})
|
|
345
|
+
|
|
346
|
+
if method == "initialize":
|
|
347
|
+
return {
|
|
348
|
+
"protocolVersion": "2024-11-05",
|
|
349
|
+
"capabilities": {"tools": {}},
|
|
350
|
+
"serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
|
|
351
|
+
}
|
|
352
|
+
elif method == "tools/list":
|
|
353
|
+
return {"tools": build_tool_definitions()}
|
|
354
|
+
elif method == "tools/call":
|
|
355
|
+
tool_name = params.get("name")
|
|
356
|
+
arguments = params.get("arguments", {})
|
|
357
|
+
result = await execute_tool(tool_name, arguments)
|
|
358
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
|
359
|
+
elif method == "ping":
|
|
360
|
+
return {}
|
|
361
|
+
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
async def main():
|
|
365
|
+
reader = asyncio.StreamReader()
|
|
366
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
367
|
+
loop = asyncio.get_event_loop()
|
|
368
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
369
|
+
|
|
370
|
+
logger.info(f"{TOOL_NAME} MCP server started")
|
|
371
|
+
|
|
372
|
+
while True:
|
|
373
|
+
line = await reader.readline()
|
|
374
|
+
if not line:
|
|
375
|
+
break
|
|
376
|
+
|
|
377
|
+
line_str = line.decode("utf-8").strip()
|
|
378
|
+
if not line_str:
|
|
379
|
+
continue
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
request = json.loads(line_str)
|
|
383
|
+
response = await handle_request(request)
|
|
384
|
+
response["jsonrpc"] = "2.0"
|
|
385
|
+
response["id"] = request.get("id")
|
|
386
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
387
|
+
sys.stdout.flush()
|
|
388
|
+
except json.JSONDecodeError:
|
|
389
|
+
error_resp = {
|
|
390
|
+
"jsonrpc": "2.0",
|
|
391
|
+
"id": None,
|
|
392
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
393
|
+
}
|
|
394
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
395
|
+
sys.stdout.flush()
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.exception("Error handling request")
|
|
398
|
+
error_resp = {
|
|
399
|
+
"jsonrpc": "2.0",
|
|
400
|
+
"id": request.get("id") if "request" in dir() else None,
|
|
401
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"}
|
|
402
|
+
}
|
|
403
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
404
|
+
sys.stdout.flush()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
asyncio.run(main())
|