shadowcat 2.0.0__py3-none-any.whl
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.
- agent/__init__.py +17 -0
- agent/benchmark/__init__.py +11 -0
- agent/benchmark/cli.py +179 -0
- agent/benchmark/config.py +15 -0
- agent/benchmark/docker.py +192 -0
- agent/benchmark/registry.py +99 -0
- agent/core/__init__.py +0 -0
- agent/core/agent.py +362 -0
- agent/core/backend.py +1667 -0
- agent/core/config.py +106 -0
- agent/core/controller.py +638 -0
- agent/core/events.py +177 -0
- agent/core/langfuse.py +320 -0
- agent/core/phantom.py +2327 -0
- agent/core/planner.py +493 -0
- agent/core/profiling.py +58 -0
- agent/core/sanitizer.py +104 -0
- agent/core/session.py +228 -0
- agent/core/tracer.py +137 -0
- agent/interface/__init__.py +0 -0
- agent/interface/components/__init__.py +0 -0
- agent/interface/components/activity_feed.py +202 -0
- agent/interface/components/renderers.py +149 -0
- agent/interface/components/splash.py +112 -0
- agent/interface/main.py +1126 -0
- agent/interface/styles.tcss +421 -0
- agent/interface/tui.py +508 -0
- agent/parsing/html_distiller.py +230 -0
- agent/parsing/tool_parser.py +115 -0
- agent/prompts/__init__.py +0 -0
- agent/prompts/pentesting.py +238 -0
- agent/rag_module/knowledge_base.py +116 -0
- agent/tests/benchmark_phantom.py +455 -0
- agent/tools/__init__.py +14 -0
- agent/tools/base.py +99 -0
- agent/tools/executor.py +345 -0
- agent/tools/registry.py +47 -0
- backend/README.md +244 -0
- backend/__init__.py +10 -0
- backend/api/__init__.py +1 -0
- backend/api/routes_scan.py +932 -0
- backend/authz.py +75 -0
- backend/cli.py +261 -0
- backend/compliance/__init__.py +1 -0
- backend/compliance/pdpa_mapping.py +16 -0
- backend/core/__init__.py +1 -0
- backend/core/coverage.py +93 -0
- backend/core/events.py +166 -0
- backend/core/llm_client.py +614 -0
- backend/core/orchestrator.py +402 -0
- backend/core/scan_memory.py +236 -0
- backend/crawler/__init__.py +1 -0
- backend/crawler/csrf.py +75 -0
- backend/crawler/headless.py +232 -0
- backend/crawler/js_analyzer.py +133 -0
- backend/crawler/spider.py +550 -0
- backend/crawler/subdomains.py +279 -0
- backend/daemon.py +252 -0
- backend/db/README.md +86 -0
- backend/db/__init__.py +17 -0
- backend/db/engine.py +87 -0
- backend/db/models.py +206 -0
- backend/db/repository.py +316 -0
- backend/db/schema.sql +247 -0
- backend/modes/__init__.py +5 -0
- backend/modes/base.py +178 -0
- backend/modes/ctf.py +39 -0
- backend/modes/enterprise.py +210 -0
- backend/modes/general.py +226 -0
- backend/modes/registry.py +34 -0
- backend/reporting/__init__.py +0 -0
- backend/reporting/generator.py +285 -0
- backend/schemas/__init__.py +1 -0
- backend/schemas/api.py +423 -0
- backend/tools/__init__.py +62 -0
- backend/tools/dirbrute_tool.py +304 -0
- backend/tools/general_report_tool.py +135 -0
- backend/tools/http_tool.py +351 -0
- backend/tools/nuclei_tool.py +20 -0
- backend/tools/report_tool.py +145 -0
- backend/tools/shell_tools.py +23 -0
- backend/verification/__init__.py +1 -0
- backend/verification/evidence_store.py +125 -0
- backend/verification/general_oracle.py +369 -0
- backend/verification/idor_oracle.py +131 -0
- backend/waf/__init__.py +0 -0
- backend/waf/detector.py +147 -0
- backend/waf/evasion.py +117 -0
- backend/webui/index.html +713 -0
- backend/workspace.py +182 -0
- shadowcat-2.0.0.dist-info/METADATA +360 -0
- shadowcat-2.0.0.dist-info/RECORD +95 -0
- shadowcat-2.0.0.dist-info/WHEEL +4 -0
- shadowcat-2.0.0.dist-info/entry_points.txt +4 -0
- shadowcat-2.0.0.dist-info/licenses/LICENSE.md +21 -0
agent/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""ShadowCat - AI-Powered Penetration Testing Assistant."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.0.0"
|
|
4
|
+
__author__ = "Your Name"
|
|
5
|
+
__license__ = "MIT"
|
|
6
|
+
|
|
7
|
+
from agent.core.agent import run_pentest
|
|
8
|
+
from agent.core.config import ShadowCatConfig, load_config
|
|
9
|
+
from agent.core.tracer import Tracer, get_global_tracer
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ShadowCatConfig",
|
|
13
|
+
"Tracer",
|
|
14
|
+
"get_global_tracer",
|
|
15
|
+
"load_config",
|
|
16
|
+
"run_pentest",
|
|
17
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Simple benchmark manager for ShadowCat.
|
|
2
|
+
|
|
3
|
+
Start/stop benchmark containers and expose ports for manual testing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from agent.benchmark.registry import BenchmarkInfo, BenchmarkRegistry
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BenchmarkInfo",
|
|
10
|
+
"BenchmarkRegistry",
|
|
11
|
+
]
|
agent/benchmark/cli.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Simple CLI for benchmark management.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
shadowcat_agent-benchmark list [--tags TAG ...] [--levels N ...]
|
|
5
|
+
shadowcat_agent-benchmark start BENCHMARK_ID
|
|
6
|
+
shadowcat_agent-benchmark stop BENCHMARK_ID
|
|
7
|
+
shadowcat_agent-benchmark status
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from agent.benchmark.config import DEFAULT_BENCHMARKS_DIR
|
|
15
|
+
from agent.benchmark.docker import (
|
|
16
|
+
get_running_benchmarks,
|
|
17
|
+
start_benchmark,
|
|
18
|
+
stop_benchmark,
|
|
19
|
+
)
|
|
20
|
+
from agent.benchmark.registry import BenchmarkRegistry
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
24
|
+
"""List available benchmarks."""
|
|
25
|
+
benchmarks_dir = Path(args.benchmarks_dir) if args.benchmarks_dir else DEFAULT_BENCHMARKS_DIR
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
registry = BenchmarkRegistry(benchmarks_dir)
|
|
29
|
+
registry.load()
|
|
30
|
+
except FileNotFoundError as e:
|
|
31
|
+
print(f"Error: {e}")
|
|
32
|
+
return 1
|
|
33
|
+
|
|
34
|
+
# Show tags if requested
|
|
35
|
+
if args.show_tags:
|
|
36
|
+
print("Available tags:")
|
|
37
|
+
for tag in sorted(registry.get_all_tags()):
|
|
38
|
+
count = len(registry.filter(tags=[tag]))
|
|
39
|
+
print(f" {tag}: {count} benchmarks")
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
# Filter and list
|
|
43
|
+
benchmarks = registry.filter(tags=args.tags, levels=args.levels)
|
|
44
|
+
|
|
45
|
+
print(f"{'ID':<15} {'Level':^5} {'Name':<40} {'Tags'}")
|
|
46
|
+
print("-" * 80)
|
|
47
|
+
for b in benchmarks:
|
|
48
|
+
tags_str = ", ".join(b.tags[:3])
|
|
49
|
+
if len(b.tags) > 3:
|
|
50
|
+
tags_str += "..."
|
|
51
|
+
print(f"{b.id:<15} {b.level:^5} {b.name[:40]:<40} {tags_str}")
|
|
52
|
+
|
|
53
|
+
print(f"\nTotal: {len(benchmarks)} benchmarks")
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cmd_start(args: argparse.Namespace) -> int:
|
|
58
|
+
"""Start a benchmark."""
|
|
59
|
+
benchmarks_dir = Path(args.benchmarks_dir) if args.benchmarks_dir else DEFAULT_BENCHMARKS_DIR
|
|
60
|
+
|
|
61
|
+
registry = BenchmarkRegistry(benchmarks_dir)
|
|
62
|
+
benchmark = registry.get(args.benchmark_id)
|
|
63
|
+
|
|
64
|
+
if not benchmark:
|
|
65
|
+
print(f"Error: Benchmark '{args.benchmark_id}' not found")
|
|
66
|
+
return 1
|
|
67
|
+
|
|
68
|
+
print(f"Starting benchmark: {benchmark.name}")
|
|
69
|
+
print(f"Description: {benchmark.description}")
|
|
70
|
+
print(f"Level: {benchmark.level}, Tags: {', '.join(benchmark.tags)}")
|
|
71
|
+
print()
|
|
72
|
+
|
|
73
|
+
result = start_benchmark(benchmark.path)
|
|
74
|
+
|
|
75
|
+
if result["success"]:
|
|
76
|
+
target_url = result["target_url"]
|
|
77
|
+
# Extract port from URL for Docker target
|
|
78
|
+
port = target_url.split(":")[-1]
|
|
79
|
+
docker_url = f"http://host.docker.internal:{port}"
|
|
80
|
+
|
|
81
|
+
print("\nBenchmark started successfully!")
|
|
82
|
+
print(f"Target URL: {target_url}")
|
|
83
|
+
print()
|
|
84
|
+
print("Run ShadowCat against this target:")
|
|
85
|
+
print(f" Local: shadowcat_agent --target {target_url}")
|
|
86
|
+
print(f" Docker: shadowcat_agent --target {docker_url}")
|
|
87
|
+
else:
|
|
88
|
+
print(f"\nFailed to start benchmark: {result['message']}")
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_stop(args: argparse.Namespace) -> int:
|
|
95
|
+
"""Stop a benchmark."""
|
|
96
|
+
benchmarks_dir = Path(args.benchmarks_dir) if args.benchmarks_dir else DEFAULT_BENCHMARKS_DIR
|
|
97
|
+
|
|
98
|
+
registry = BenchmarkRegistry(benchmarks_dir)
|
|
99
|
+
benchmark = registry.get(args.benchmark_id)
|
|
100
|
+
|
|
101
|
+
if not benchmark:
|
|
102
|
+
print(f"Error: Benchmark '{args.benchmark_id}' not found")
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
result = stop_benchmark(benchmark.path)
|
|
106
|
+
|
|
107
|
+
if result["success"]:
|
|
108
|
+
print("Benchmark stopped successfully")
|
|
109
|
+
else:
|
|
110
|
+
print(f"Failed to stop benchmark: {result['message']}")
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_status(args: argparse.Namespace) -> int:
|
|
117
|
+
"""Show running benchmarks."""
|
|
118
|
+
running = get_running_benchmarks()
|
|
119
|
+
|
|
120
|
+
if not running:
|
|
121
|
+
print("No benchmark containers currently running")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
print("Running benchmark containers:")
|
|
125
|
+
print(f"{'Name':<40} {'Ports':<30} {'Status'}")
|
|
126
|
+
print("-" * 80)
|
|
127
|
+
for container in running:
|
|
128
|
+
print(f"{container['name']:<40} {container['ports']:<30} {container['status']}")
|
|
129
|
+
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main() -> None:
|
|
134
|
+
"""Main entry point."""
|
|
135
|
+
parser = argparse.ArgumentParser(
|
|
136
|
+
prog="shadowcat_agent-benchmark",
|
|
137
|
+
description="Manage benchmark containers for ShadowCat testing",
|
|
138
|
+
)
|
|
139
|
+
parser.add_argument(
|
|
140
|
+
"--benchmarks-dir",
|
|
141
|
+
"-d",
|
|
142
|
+
help="Path to benchmarks directory",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
146
|
+
|
|
147
|
+
# List command
|
|
148
|
+
list_parser = subparsers.add_parser("list", help="List available benchmarks")
|
|
149
|
+
list_parser.add_argument("--tags", "-t", nargs="+", help="Filter by tags")
|
|
150
|
+
list_parser.add_argument(
|
|
151
|
+
"--levels", "-l", nargs="+", type=int, help="Filter by levels (1, 2, 3)"
|
|
152
|
+
)
|
|
153
|
+
list_parser.add_argument("--show-tags", action="store_true", help="Show all tags")
|
|
154
|
+
|
|
155
|
+
# Start command
|
|
156
|
+
start_parser = subparsers.add_parser("start", help="Start a benchmark")
|
|
157
|
+
start_parser.add_argument("benchmark_id", help="Benchmark ID (e.g., XBEN-001-24)")
|
|
158
|
+
|
|
159
|
+
# Stop command
|
|
160
|
+
stop_parser = subparsers.add_parser("stop", help="Stop a benchmark")
|
|
161
|
+
stop_parser.add_argument("benchmark_id", help="Benchmark ID")
|
|
162
|
+
|
|
163
|
+
# Status command
|
|
164
|
+
subparsers.add_parser("status", help="Show running benchmarks")
|
|
165
|
+
|
|
166
|
+
args = parser.parse_args()
|
|
167
|
+
|
|
168
|
+
if args.command == "list":
|
|
169
|
+
sys.exit(cmd_list(args))
|
|
170
|
+
elif args.command == "start":
|
|
171
|
+
sys.exit(cmd_start(args))
|
|
172
|
+
elif args.command == "stop":
|
|
173
|
+
sys.exit(cmd_stop(args))
|
|
174
|
+
elif args.command == "status":
|
|
175
|
+
sys.exit(cmd_status(args))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Simple configuration for benchmark manager."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Default benchmarks directory (relative to project root)
|
|
6
|
+
# Use xbow-validation-benchmarks as the default benchmark suite
|
|
7
|
+
DEFAULT_BENCHMARKS_DIR = (
|
|
8
|
+
Path(__file__).parent.parent.parent / "benchmark" / "xbow-validation-benchmarks" / "benchmarks"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Default host to bind ports
|
|
12
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
13
|
+
|
|
14
|
+
# Default port for single benchmark (can be overridden)
|
|
15
|
+
DEFAULT_PORT = 8080
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Simple Docker manager for benchmarks.
|
|
2
|
+
|
|
3
|
+
Starts/stops benchmark containers with ports exposed to localhost.
|
|
4
|
+
|
|
5
|
+
Async versions (``async_*`` prefix) are the primary implementation.
|
|
6
|
+
The original sync names are thin wrappers around ``asyncio.run()`` so
|
|
7
|
+
existing call-sites remain unchanged. Call the ``async_*`` forms directly
|
|
8
|
+
when already inside an event loop.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from agent.benchmark.config import DEFAULT_HOST, DEFAULT_PORT
|
|
19
|
+
from agent.tools.executor import AsyncToolExecutor
|
|
20
|
+
|
|
21
|
+
# One shared executor for all Docker operations; Docker I/O is heavy so we
|
|
22
|
+
# cap concurrency at 3 and give long-running builds up to 10 minutes.
|
|
23
|
+
_docker_executor = AsyncToolExecutor(max_workers=3, default_timeout=600.0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def async_start_benchmark(benchmark_path: Path, port: int = DEFAULT_PORT) -> dict[str, Any]:
|
|
27
|
+
"""Start a benchmark container (async).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
benchmark_path: Path to benchmark directory (contains docker-compose.yml)
|
|
31
|
+
port: Port to expose on host (default: 8080)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
dict with 'success', 'target_url', and 'message'
|
|
35
|
+
"""
|
|
36
|
+
if not benchmark_path.exists():
|
|
37
|
+
return {
|
|
38
|
+
"success": False,
|
|
39
|
+
"target_url": None,
|
|
40
|
+
"message": f"Path not found: {benchmark_path}",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compose_file = benchmark_path / "docker-compose.yml"
|
|
44
|
+
if not compose_file.exists():
|
|
45
|
+
return {"success": False, "target_url": None, "message": "No docker-compose.yml found"}
|
|
46
|
+
|
|
47
|
+
print(f"Building benchmark at {benchmark_path.name}...")
|
|
48
|
+
build_result = await _docker_executor.run(
|
|
49
|
+
["make", "build"],
|
|
50
|
+
cwd=str(benchmark_path),
|
|
51
|
+
timeout=600.0,
|
|
52
|
+
shell=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not build_result.success:
|
|
56
|
+
return {
|
|
57
|
+
"success": False,
|
|
58
|
+
"target_url": None,
|
|
59
|
+
"message": f"Build failed: {build_result.stderr or build_result.stdout}",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
print(f"Starting containers on port {port}...")
|
|
63
|
+
up_result = await _docker_executor.run(
|
|
64
|
+
["docker", "compose", "up", "-d", "--wait"],
|
|
65
|
+
cwd=str(benchmark_path),
|
|
66
|
+
timeout=120.0,
|
|
67
|
+
shell=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not up_result.success:
|
|
71
|
+
return {
|
|
72
|
+
"success": False,
|
|
73
|
+
"target_url": None,
|
|
74
|
+
"message": f"Start failed: {up_result.stderr or up_result.stdout}",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
actual_port = await async_get_exposed_port(benchmark_path)
|
|
78
|
+
target_url = f"http://{DEFAULT_HOST}:{actual_port or port}"
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"success": True,
|
|
82
|
+
"target_url": target_url,
|
|
83
|
+
"port": actual_port or port,
|
|
84
|
+
"message": f"Benchmark started at {target_url}",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def async_stop_benchmark(benchmark_path: Path) -> dict[str, Any]:
|
|
89
|
+
"""Stop a benchmark container (async).
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
benchmark_path: Path to benchmark directory
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict with 'success' and 'message'
|
|
96
|
+
"""
|
|
97
|
+
if not benchmark_path.exists():
|
|
98
|
+
return {"success": False, "message": f"Path not found: {benchmark_path}"}
|
|
99
|
+
|
|
100
|
+
print(f"Stopping benchmark at {benchmark_path.name}...")
|
|
101
|
+
result = await _docker_executor.run(
|
|
102
|
+
["docker", "compose", "down", "--remove-orphans"],
|
|
103
|
+
cwd=str(benchmark_path),
|
|
104
|
+
timeout=60.0,
|
|
105
|
+
shell=False,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if not result.success:
|
|
109
|
+
return {"success": False, "message": f"Stop failed: {result.stderr or result.stdout}"}
|
|
110
|
+
|
|
111
|
+
return {"success": True, "message": "Benchmark stopped"}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def async_get_exposed_port(benchmark_path: Path) -> int | None:
|
|
115
|
+
"""Get the exposed port from running containers (async).
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
benchmark_path: Path to benchmark directory
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Port number or None if not found
|
|
122
|
+
"""
|
|
123
|
+
result = await _docker_executor.run(
|
|
124
|
+
["docker", "compose", "ps", "--format", "{{.Ports}}"],
|
|
125
|
+
cwd=str(benchmark_path),
|
|
126
|
+
timeout=15.0,
|
|
127
|
+
shell=False,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not result.success or not result.stdout.strip():
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
for line in result.stdout.strip().split("\n"):
|
|
134
|
+
match = re.search(r"0\.0\.0\.0:(\d+)->", line)
|
|
135
|
+
if match:
|
|
136
|
+
return int(match.group(1))
|
|
137
|
+
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def async_get_running_benchmarks() -> list[dict[str, Any]]:
|
|
142
|
+
"""Get list of running benchmark containers (async).
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of dicts with container info
|
|
146
|
+
"""
|
|
147
|
+
result = await _docker_executor.run(
|
|
148
|
+
["docker", "ps", "--format", "{{.Names}}\t{{.Ports}}\t{{.Status}}"],
|
|
149
|
+
timeout=15.0,
|
|
150
|
+
shell=False,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if not result.success:
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
running = []
|
|
157
|
+
for line in result.stdout.strip().split("\n"):
|
|
158
|
+
if not line:
|
|
159
|
+
continue
|
|
160
|
+
parts = line.split("\t")
|
|
161
|
+
if len(parts) >= 3:
|
|
162
|
+
name, ports, status = parts[0], parts[1], parts[2]
|
|
163
|
+
if "xben" in name.lower():
|
|
164
|
+
running.append({"name": name, "ports": ports, "status": status})
|
|
165
|
+
|
|
166
|
+
return running
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Synchronous wrappers (backward-compatible call-sites)
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
# These must NOT be called from within a running event loop.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def start_benchmark(benchmark_path: Path, port: int = DEFAULT_PORT) -> dict[str, Any]:
|
|
176
|
+
"""Synchronous wrapper around async_start_benchmark."""
|
|
177
|
+
return asyncio.run(async_start_benchmark(benchmark_path, port))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def stop_benchmark(benchmark_path: Path) -> dict[str, Any]:
|
|
181
|
+
"""Synchronous wrapper around async_stop_benchmark."""
|
|
182
|
+
return asyncio.run(async_stop_benchmark(benchmark_path))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_exposed_port(benchmark_path: Path) -> int | None:
|
|
186
|
+
"""Synchronous wrapper around async_get_exposed_port."""
|
|
187
|
+
return asyncio.run(async_get_exposed_port(benchmark_path))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_running_benchmarks() -> list[dict[str, Any]]:
|
|
191
|
+
"""Synchronous wrapper around async_get_running_benchmarks."""
|
|
192
|
+
return asyncio.run(async_get_running_benchmarks())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Simple benchmark registry - discovers benchmarks from directory."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from agent.benchmark.config import DEFAULT_BENCHMARKS_DIR
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BenchmarkInfo:
|
|
12
|
+
"""Information about a benchmark."""
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
name: str
|
|
16
|
+
description: str
|
|
17
|
+
level: int
|
|
18
|
+
tags: list[str]
|
|
19
|
+
path: Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BenchmarkRegistry:
|
|
23
|
+
"""Discovers benchmarks from a directory."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, benchmarks_dir: Path | None = None):
|
|
26
|
+
self.benchmarks_dir = benchmarks_dir or DEFAULT_BENCHMARKS_DIR
|
|
27
|
+
self._benchmarks: dict[str, BenchmarkInfo] = {}
|
|
28
|
+
|
|
29
|
+
def load(self) -> None:
|
|
30
|
+
"""Load all benchmarks from directory."""
|
|
31
|
+
self._benchmarks.clear()
|
|
32
|
+
|
|
33
|
+
if not self.benchmarks_dir.exists():
|
|
34
|
+
raise FileNotFoundError(f"Benchmarks directory not found: {self.benchmarks_dir}")
|
|
35
|
+
|
|
36
|
+
for benchmark_path in sorted(self.benchmarks_dir.iterdir()):
|
|
37
|
+
if not benchmark_path.is_dir():
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
benchmark_json = benchmark_path / "benchmark.json"
|
|
41
|
+
if not benchmark_json.exists():
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with open(benchmark_json) as f:
|
|
46
|
+
data = json.load(f)
|
|
47
|
+
|
|
48
|
+
info = BenchmarkInfo(
|
|
49
|
+
id=benchmark_path.name,
|
|
50
|
+
name=data.get("name", benchmark_path.name),
|
|
51
|
+
description=data.get("description", ""),
|
|
52
|
+
level=int(data.get("level", 1)),
|
|
53
|
+
tags=data.get("tags", []),
|
|
54
|
+
path=benchmark_path,
|
|
55
|
+
)
|
|
56
|
+
self._benchmarks[info.id] = info
|
|
57
|
+
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
58
|
+
print(f"Warning: Failed to parse {benchmark_json}: {e}")
|
|
59
|
+
|
|
60
|
+
def get(self, benchmark_id: str) -> BenchmarkInfo | None:
|
|
61
|
+
"""Get benchmark by ID."""
|
|
62
|
+
if not self._benchmarks:
|
|
63
|
+
self.load()
|
|
64
|
+
return self._benchmarks.get(benchmark_id)
|
|
65
|
+
|
|
66
|
+
def list_all(self) -> list[BenchmarkInfo]:
|
|
67
|
+
"""List all benchmarks."""
|
|
68
|
+
if not self._benchmarks:
|
|
69
|
+
self.load()
|
|
70
|
+
return sorted(self._benchmarks.values(), key=lambda b: b.id)
|
|
71
|
+
|
|
72
|
+
def filter(
|
|
73
|
+
self,
|
|
74
|
+
tags: list[str] | None = None,
|
|
75
|
+
levels: list[int] | None = None,
|
|
76
|
+
) -> list[BenchmarkInfo]:
|
|
77
|
+
"""Filter benchmarks by tags or levels."""
|
|
78
|
+
if not self._benchmarks:
|
|
79
|
+
self.load()
|
|
80
|
+
|
|
81
|
+
result = list(self._benchmarks.values())
|
|
82
|
+
|
|
83
|
+
if levels:
|
|
84
|
+
result = [b for b in result if b.level in levels]
|
|
85
|
+
|
|
86
|
+
if tags:
|
|
87
|
+
tags_lower = [t.lower() for t in tags]
|
|
88
|
+
result = [b for b in result if any(t.lower() in tags_lower for t in b.tags)]
|
|
89
|
+
|
|
90
|
+
return sorted(result, key=lambda b: b.id)
|
|
91
|
+
|
|
92
|
+
def get_all_tags(self) -> set[str]:
|
|
93
|
+
"""Get all unique tags."""
|
|
94
|
+
if not self._benchmarks:
|
|
95
|
+
self.load()
|
|
96
|
+
tags: set[str] = set()
|
|
97
|
+
for b in self._benchmarks.values():
|
|
98
|
+
tags.update(b.tags)
|
|
99
|
+
return tags
|
agent/core/__init__.py
ADDED
|
File without changes
|