zen-ai-pentest 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.
- agents/__init__.py +28 -0
- agents/agent_base.py +239 -0
- agents/agent_orchestrator.py +346 -0
- agents/analysis_agent.py +225 -0
- agents/cli.py +258 -0
- agents/exploit_agent.py +224 -0
- agents/integration.py +211 -0
- agents/post_scan_agent.py +937 -0
- agents/react_agent.py +384 -0
- agents/react_agent_enhanced.py +616 -0
- agents/react_agent_vm.py +298 -0
- agents/research_agent.py +176 -0
- api/__init__.py +11 -0
- api/auth.py +123 -0
- api/main.py +1027 -0
- api/schemas.py +357 -0
- api/websocket.py +97 -0
- autonomous/__init__.py +122 -0
- autonomous/agent.py +253 -0
- autonomous/agent_loop.py +1370 -0
- autonomous/exploit_validator.py +1537 -0
- autonomous/memory.py +448 -0
- autonomous/react.py +339 -0
- autonomous/tool_executor.py +488 -0
- backends/__init__.py +16 -0
- backends/chatgpt_direct.py +133 -0
- backends/claude_direct.py +130 -0
- backends/duckduckgo.py +138 -0
- backends/openrouter.py +120 -0
- benchmarks/__init__.py +149 -0
- benchmarks/benchmark_engine.py +904 -0
- benchmarks/ci_benchmark.py +785 -0
- benchmarks/comparison.py +729 -0
- benchmarks/metrics.py +553 -0
- benchmarks/run_benchmarks.py +809 -0
- ci_cd/__init__.py +2 -0
- core/__init__.py +17 -0
- core/async_pool.py +282 -0
- core/asyncio_fix.py +222 -0
- core/cache.py +472 -0
- core/container.py +277 -0
- core/database.py +114 -0
- core/input_validator.py +353 -0
- core/models.py +288 -0
- core/orchestrator.py +611 -0
- core/plugin_manager.py +571 -0
- core/rate_limiter.py +405 -0
- core/secure_config.py +328 -0
- core/shield_integration.py +296 -0
- modules/__init__.py +46 -0
- modules/cve_database.py +362 -0
- modules/exploit_assist.py +330 -0
- modules/nuclei_integration.py +480 -0
- modules/osint.py +604 -0
- modules/protonvpn.py +554 -0
- modules/recon.py +165 -0
- modules/sql_injection_db.py +826 -0
- modules/tool_orchestrator.py +498 -0
- modules/vuln_scanner.py +292 -0
- modules/wordlist_generator.py +566 -0
- risk_engine/__init__.py +99 -0
- risk_engine/business_impact.py +267 -0
- risk_engine/business_impact_calculator.py +563 -0
- risk_engine/cvss.py +156 -0
- risk_engine/epss.py +190 -0
- risk_engine/example_usage.py +294 -0
- risk_engine/false_positive_engine.py +1073 -0
- risk_engine/scorer.py +304 -0
- web_ui/backend/main.py +471 -0
- zen_ai_pentest-2.0.0.dist-info/METADATA +795 -0
- zen_ai_pentest-2.0.0.dist-info/RECORD +75 -0
- zen_ai_pentest-2.0.0.dist-info/WHEEL +5 -0
- zen_ai_pentest-2.0.0.dist-info/entry_points.txt +2 -0
- zen_ai_pentest-2.0.0.dist-info/licenses/LICENSE +21 -0
- zen_ai_pentest-2.0.0.dist-info/top_level.txt +10 -0
core/plugin_manager.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Plugin Manager - Dynamic Plugin System for Zen AI Pentest
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Dynamic plugin loading/unloading
|
|
6
|
+
- Hook system for extensibility
|
|
7
|
+
- Plugin configuration management
|
|
8
|
+
- Sandboxed execution
|
|
9
|
+
- Dependency checking
|
|
10
|
+
|
|
11
|
+
Author: SHAdd0WTAka + Kimi AI
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import importlib
|
|
15
|
+
import importlib.util
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import pkgutil
|
|
20
|
+
import sys
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Callable, Dict, List, Optional, Type
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("ZenAI.Plugins")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PluginType(Enum):
|
|
31
|
+
"""Types of plugins"""
|
|
32
|
+
SCANNER = "scanner" # Vulnerability scanners
|
|
33
|
+
EXPLOIT = "exploit" # Exploit modules
|
|
34
|
+
REPORT = "report" # Report generators
|
|
35
|
+
OSINT = "osint" # OSINT sources
|
|
36
|
+
POST_EXPLOITATION = "post" # Post-exploitation
|
|
37
|
+
TOOL = "tool" # External tools integration
|
|
38
|
+
NOTIFIER = "notifier" # Notification services
|
|
39
|
+
AUTH = "auth" # Authentication providers
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PluginStatus(Enum):
|
|
43
|
+
"""Plugin loading status"""
|
|
44
|
+
UNLOADED = "unloaded"
|
|
45
|
+
LOADING = "loading"
|
|
46
|
+
LOADED = "loaded"
|
|
47
|
+
ERROR = "error"
|
|
48
|
+
DISABLED = "disabled"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PluginInfo:
|
|
53
|
+
"""Plugin metadata"""
|
|
54
|
+
name: str
|
|
55
|
+
version: str
|
|
56
|
+
description: str
|
|
57
|
+
author: str
|
|
58
|
+
plugin_type: PluginType
|
|
59
|
+
dependencies: List[str] = field(default_factory=list)
|
|
60
|
+
hooks: List[str] = field(default_factory=list)
|
|
61
|
+
config_schema: Dict[str, Any] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
# Runtime info
|
|
64
|
+
status: PluginStatus = PluginStatus.UNLOADED
|
|
65
|
+
path: Optional[str] = None
|
|
66
|
+
error_message: Optional[str] = None
|
|
67
|
+
loaded_at: Optional[str] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BasePlugin(ABC):
|
|
71
|
+
"""
|
|
72
|
+
Base class for all plugins
|
|
73
|
+
|
|
74
|
+
All plugins must inherit from this class and implement required methods.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
# Plugin metadata (must be overridden)
|
|
78
|
+
NAME: str = ""
|
|
79
|
+
VERSION: str = "1.0.0"
|
|
80
|
+
DESCRIPTION: str = ""
|
|
81
|
+
AUTHOR: str = ""
|
|
82
|
+
PLUGIN_TYPE: PluginType = PluginType.TOOL
|
|
83
|
+
|
|
84
|
+
def __init__(self, config: Optional[Dict] = None):
|
|
85
|
+
self.config = config or {}
|
|
86
|
+
self.enabled = True
|
|
87
|
+
self.logger = logging.getLogger(f"ZenAI.Plugin.{self.NAME}")
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
async def initialize(self) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Initialize the plugin.
|
|
93
|
+
Called once when plugin is loaded.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if initialization successful
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
async def execute(self, **kwargs) -> Any:
|
|
102
|
+
"""
|
|
103
|
+
Execute plugin functionality.
|
|
104
|
+
Main entry point for the plugin.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
**kwargs: Plugin-specific parameters
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Plugin result
|
|
111
|
+
"""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
async def shutdown(self):
|
|
115
|
+
"""
|
|
116
|
+
Cleanup when plugin is unloaded.
|
|
117
|
+
Override to perform cleanup.
|
|
118
|
+
"""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def get_config(self, key: str, default: Any = None) -> Any:
|
|
122
|
+
"""Get configuration value"""
|
|
123
|
+
return self.config.get(key, default)
|
|
124
|
+
|
|
125
|
+
def set_config(self, key: str, value: Any):
|
|
126
|
+
"""Set configuration value"""
|
|
127
|
+
self.config[key] = value
|
|
128
|
+
|
|
129
|
+
def validate_config(self) -> bool:
|
|
130
|
+
"""
|
|
131
|
+
Validate plugin configuration.
|
|
132
|
+
Override to implement custom validation.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
True if configuration is valid
|
|
136
|
+
"""
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def get_info(self) -> PluginInfo:
|
|
140
|
+
"""Get plugin information"""
|
|
141
|
+
return PluginInfo(
|
|
142
|
+
name=self.NAME,
|
|
143
|
+
version=self.VERSION,
|
|
144
|
+
description=self.DESCRIPTION,
|
|
145
|
+
author=self.AUTHOR,
|
|
146
|
+
plugin_type=self.PLUGIN_TYPE
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class HookManager:
|
|
151
|
+
"""
|
|
152
|
+
Hook system for plugin intercommunication.
|
|
153
|
+
Allows plugins to register callbacks for specific events.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self):
|
|
157
|
+
self._hooks: Dict[str, List[Callable]] = {}
|
|
158
|
+
self._filters: Dict[str, List[Callable]] = {}
|
|
159
|
+
|
|
160
|
+
def register_hook(self, hook_name: str, callback: Callable, priority: int = 10):
|
|
161
|
+
"""
|
|
162
|
+
Register a hook callback.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
hook_name: Name of the hook
|
|
166
|
+
callback: Function to call
|
|
167
|
+
priority: Lower number = higher priority (default 10)
|
|
168
|
+
"""
|
|
169
|
+
if hook_name not in self._hooks:
|
|
170
|
+
self._hooks[hook_name] = []
|
|
171
|
+
|
|
172
|
+
self._hooks[hook_name].append({
|
|
173
|
+
"callback": callback,
|
|
174
|
+
"priority": priority
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
# Sort by priority
|
|
178
|
+
self._hooks[hook_name].sort(key=lambda x: x["priority"])
|
|
179
|
+
logger.debug(f"Registered hook '{hook_name}' with priority {priority}")
|
|
180
|
+
|
|
181
|
+
def unregister_hook(self, hook_name: str, callback: Callable):
|
|
182
|
+
"""Unregister a hook callback"""
|
|
183
|
+
if hook_name in self._hooks:
|
|
184
|
+
self._hooks[hook_name] = [
|
|
185
|
+
h for h in self._hooks[hook_name]
|
|
186
|
+
if h["callback"] != callback
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
async def execute_hook(self, hook_name: str, *args, **kwargs) -> List[Any]:
|
|
190
|
+
"""
|
|
191
|
+
Execute all callbacks for a hook.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
hook_name: Name of the hook to execute
|
|
195
|
+
*args, **kwargs: Arguments to pass to callbacks
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
List of results from all callbacks
|
|
199
|
+
"""
|
|
200
|
+
results = []
|
|
201
|
+
|
|
202
|
+
if hook_name in self._hooks:
|
|
203
|
+
for hook in self._hooks[hook_name]:
|
|
204
|
+
try:
|
|
205
|
+
result = await hook["callback"](*args, **kwargs)
|
|
206
|
+
results.append(result)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.error(f"Hook '{hook_name}' callback failed: {e}")
|
|
209
|
+
|
|
210
|
+
return results
|
|
211
|
+
|
|
212
|
+
def register_filter(self, filter_name: str, callback: Callable, priority: int = 10):
|
|
213
|
+
"""
|
|
214
|
+
Register a filter callback.
|
|
215
|
+
Filters modify data passed through them.
|
|
216
|
+
"""
|
|
217
|
+
if filter_name not in self._filters:
|
|
218
|
+
self._filters[filter_name] = []
|
|
219
|
+
|
|
220
|
+
self._filters[filter_name].append({
|
|
221
|
+
"callback": callback,
|
|
222
|
+
"priority": priority
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
self._filters[filter_name].sort(key=lambda x: x["priority"])
|
|
226
|
+
|
|
227
|
+
async def apply_filter(self, filter_name: str, data: Any, **kwargs) -> Any:
|
|
228
|
+
"""
|
|
229
|
+
Apply all filters to data.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
filter_name: Name of the filter chain
|
|
233
|
+
data: Data to filter
|
|
234
|
+
**kwargs: Additional arguments
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Filtered data
|
|
238
|
+
"""
|
|
239
|
+
if filter_name not in self._filters:
|
|
240
|
+
return data
|
|
241
|
+
|
|
242
|
+
for filter_hook in self._filters[filter_name]:
|
|
243
|
+
try:
|
|
244
|
+
data = await filter_hook["callback"](data, **kwargs)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Filter '{filter_name}' callback failed: {e}")
|
|
247
|
+
|
|
248
|
+
return data
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class PluginManager:
|
|
252
|
+
"""
|
|
253
|
+
Central plugin manager for Zen AI Pentest.
|
|
254
|
+
Handles loading, unloading, and managing plugins.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self, plugin_dirs: Optional[List[str]] = None):
|
|
258
|
+
self.plugin_dirs = plugin_dirs or ["plugins", "custom_plugins"]
|
|
259
|
+
self.plugins: Dict[str, BasePlugin] = {}
|
|
260
|
+
self.plugin_info: Dict[str, PluginInfo] = {}
|
|
261
|
+
self.hooks = HookManager()
|
|
262
|
+
self._loaded_modules: Dict[str, Any] = {}
|
|
263
|
+
|
|
264
|
+
# Ensure plugin directories exist
|
|
265
|
+
for plugin_dir in self.plugin_dirs:
|
|
266
|
+
Path(plugin_dir).mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
def discover_plugins(self) -> List[PluginInfo]:
|
|
269
|
+
"""
|
|
270
|
+
Discover available plugins in plugin directories.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of discovered plugin information
|
|
274
|
+
"""
|
|
275
|
+
discovered = []
|
|
276
|
+
|
|
277
|
+
for plugin_dir in self.plugin_dirs:
|
|
278
|
+
if not os.path.exists(plugin_dir):
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Look for plugin directories or files
|
|
282
|
+
for item in os.listdir(plugin_dir):
|
|
283
|
+
item_path = os.path.join(plugin_dir, item)
|
|
284
|
+
|
|
285
|
+
# Check for plugin.json manifest
|
|
286
|
+
manifest_path = os.path.join(item_path, "plugin.json")
|
|
287
|
+
if os.path.isfile(manifest_path):
|
|
288
|
+
try:
|
|
289
|
+
with open(manifest_path) as f:
|
|
290
|
+
manifest = json.load(f)
|
|
291
|
+
|
|
292
|
+
info = PluginInfo(
|
|
293
|
+
name=manifest.get("name", item),
|
|
294
|
+
version=manifest.get("version", "1.0.0"),
|
|
295
|
+
description=manifest.get("description", ""),
|
|
296
|
+
author=manifest.get("author", "Unknown"),
|
|
297
|
+
plugin_type=PluginType(manifest.get("type", "tool")),
|
|
298
|
+
dependencies=manifest.get("dependencies", []),
|
|
299
|
+
hooks=manifest.get("hooks", []),
|
|
300
|
+
path=item_path
|
|
301
|
+
)
|
|
302
|
+
discovered.append(info)
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f"Failed to load plugin manifest {manifest_path}: {e}")
|
|
306
|
+
|
|
307
|
+
# Check for Python module
|
|
308
|
+
elif os.path.isfile(os.path.join(item_path, "__init__.py")):
|
|
309
|
+
# Try to import and get metadata
|
|
310
|
+
try:
|
|
311
|
+
info = self._get_plugin_info_from_module(item_path)
|
|
312
|
+
if info:
|
|
313
|
+
discovered.append(info)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Failed to inspect plugin {item_path}: {e}")
|
|
316
|
+
|
|
317
|
+
return discovered
|
|
318
|
+
|
|
319
|
+
def _get_plugin_info_from_module(self, path: str) -> Optional[PluginInfo]:
|
|
320
|
+
"""Extract plugin info from Python module"""
|
|
321
|
+
# Add to path temporarily
|
|
322
|
+
parent_dir = os.path.dirname(path)
|
|
323
|
+
if parent_dir not in sys.path:
|
|
324
|
+
sys.path.insert(0, parent_dir)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
module_name = os.path.basename(path)
|
|
328
|
+
spec = importlib.util.spec_from_file_location(
|
|
329
|
+
module_name,
|
|
330
|
+
os.path.join(path, "__init__.py")
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if spec and spec.loader:
|
|
334
|
+
module = importlib.util.module_from_spec(spec)
|
|
335
|
+
spec.loader.exec_module(module)
|
|
336
|
+
|
|
337
|
+
# Look for plugin class
|
|
338
|
+
for attr_name in dir(module):
|
|
339
|
+
attr = getattr(module, attr_name)
|
|
340
|
+
if (isinstance(attr, type) and
|
|
341
|
+
issubclass(attr, BasePlugin) and
|
|
342
|
+
attr != BasePlugin and
|
|
343
|
+
hasattr(attr, 'NAME')):
|
|
344
|
+
|
|
345
|
+
return PluginInfo(
|
|
346
|
+
name=attr.NAME,
|
|
347
|
+
version=getattr(attr, 'VERSION', '1.0.0'),
|
|
348
|
+
description=getattr(attr, 'DESCRIPTION', ''),
|
|
349
|
+
author=getattr(attr, 'AUTHOR', 'Unknown'),
|
|
350
|
+
plugin_type=getattr(attr, 'PLUGIN_TYPE', PluginType.TOOL),
|
|
351
|
+
path=path
|
|
352
|
+
)
|
|
353
|
+
finally:
|
|
354
|
+
if parent_dir in sys.path:
|
|
355
|
+
sys.path.remove(parent_dir)
|
|
356
|
+
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
async def load_plugin(self, plugin_name: str, config: Optional[Dict] = None) -> bool:
|
|
360
|
+
"""
|
|
361
|
+
Load a plugin by name.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
plugin_name: Name of the plugin to load
|
|
365
|
+
config: Optional configuration dictionary
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
True if plugin loaded successfully
|
|
369
|
+
"""
|
|
370
|
+
if plugin_name in self.plugins:
|
|
371
|
+
logger.warning(f"Plugin '{plugin_name}' is already loaded")
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
# Find plugin
|
|
375
|
+
discovered = self.discover_plugins()
|
|
376
|
+
plugin_info = next((p for p in discovered if p.name == plugin_name), None)
|
|
377
|
+
|
|
378
|
+
if not plugin_info:
|
|
379
|
+
logger.error(f"Plugin '{plugin_name}' not found")
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
plugin_info.status = PluginStatus.LOADING
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
# Check dependencies
|
|
386
|
+
for dep in plugin_info.dependencies:
|
|
387
|
+
if dep not in self.plugins:
|
|
388
|
+
logger.error(f"Plugin '{plugin_name}' requires '{dep}' which is not loaded")
|
|
389
|
+
plugin_info.status = PluginStatus.ERROR
|
|
390
|
+
plugin_info.error_message = f"Missing dependency: {dep}"
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
# Load the plugin
|
|
394
|
+
plugin = await self._load_plugin_class(plugin_info, config)
|
|
395
|
+
|
|
396
|
+
if not plugin:
|
|
397
|
+
raise Exception("Failed to instantiate plugin")
|
|
398
|
+
|
|
399
|
+
# Validate config
|
|
400
|
+
if not plugin.validate_config():
|
|
401
|
+
raise Exception("Plugin configuration validation failed")
|
|
402
|
+
|
|
403
|
+
# Initialize
|
|
404
|
+
if not await plugin.initialize():
|
|
405
|
+
raise Exception("Plugin initialization failed")
|
|
406
|
+
|
|
407
|
+
# Register hooks
|
|
408
|
+
for hook_name in plugin_info.hooks:
|
|
409
|
+
if hasattr(plugin, f"on_{hook_name}"):
|
|
410
|
+
callback = getattr(plugin, f"on_{hook_name}")
|
|
411
|
+
self.hooks.register_hook(hook_name, callback)
|
|
412
|
+
|
|
413
|
+
# Store plugin
|
|
414
|
+
self.plugins[plugin_name] = plugin
|
|
415
|
+
self.plugin_info[plugin_name] = plugin_info
|
|
416
|
+
plugin_info.status = PluginStatus.LOADED
|
|
417
|
+
|
|
418
|
+
logger.info(f"Plugin '{plugin_name}' v{plugin_info.version} loaded successfully")
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.error(f"Failed to load plugin '{plugin_name}': {e}")
|
|
423
|
+
plugin_info.status = PluginStatus.ERROR
|
|
424
|
+
plugin_info.error_message = str(e)
|
|
425
|
+
return False
|
|
426
|
+
|
|
427
|
+
async def _load_plugin_class(self, info: PluginInfo, config: Optional[Dict]) -> Optional[BasePlugin]:
|
|
428
|
+
"""Load and instantiate plugin class"""
|
|
429
|
+
if not info.path:
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
parent_dir = os.path.dirname(info.path)
|
|
433
|
+
if parent_dir not in sys.path:
|
|
434
|
+
sys.path.insert(0, parent_dir)
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
module_name = os.path.basename(info.path)
|
|
438
|
+
spec = importlib.util.spec_from_file_location(
|
|
439
|
+
module_name,
|
|
440
|
+
os.path.join(info.path, "__init__.py")
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if spec and spec.loader:
|
|
444
|
+
module = importlib.util.module_from_spec(spec)
|
|
445
|
+
spec.loader.exec_module(module)
|
|
446
|
+
|
|
447
|
+
# Find plugin class
|
|
448
|
+
for attr_name in dir(module):
|
|
449
|
+
attr = getattr(module, attr_name)
|
|
450
|
+
if (isinstance(attr, type) and
|
|
451
|
+
issubclass(attr, BasePlugin) and
|
|
452
|
+
attr != BasePlugin and
|
|
453
|
+
attr.NAME == info.name):
|
|
454
|
+
|
|
455
|
+
return attr(config)
|
|
456
|
+
|
|
457
|
+
finally:
|
|
458
|
+
if parent_dir in sys.path:
|
|
459
|
+
sys.path.remove(parent_dir)
|
|
460
|
+
|
|
461
|
+
return None
|
|
462
|
+
|
|
463
|
+
async def unload_plugin(self, plugin_name: str) -> bool:
|
|
464
|
+
"""
|
|
465
|
+
Unload a plugin.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
plugin_name: Name of the plugin to unload
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
True if plugin unloaded successfully
|
|
472
|
+
"""
|
|
473
|
+
if plugin_name not in self.plugins:
|
|
474
|
+
logger.warning(f"Plugin '{plugin_name}' is not loaded")
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
plugin = self.plugins[plugin_name]
|
|
478
|
+
info = self.plugin_info[plugin_name]
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
# Shutdown plugin
|
|
482
|
+
await plugin.shutdown()
|
|
483
|
+
|
|
484
|
+
# Unregister hooks
|
|
485
|
+
for hook_name in info.hooks:
|
|
486
|
+
if hasattr(plugin, f"on_{hook_name}"):
|
|
487
|
+
callback = getattr(plugin, f"on_{hook_name}")
|
|
488
|
+
self.hooks.unregister_hook(hook_name, callback)
|
|
489
|
+
|
|
490
|
+
# Remove from registry
|
|
491
|
+
del self.plugins[plugin_name]
|
|
492
|
+
info.status = PluginStatus.UNLOADED
|
|
493
|
+
|
|
494
|
+
logger.info(f"Plugin '{plugin_name}' unloaded")
|
|
495
|
+
return True
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
logger.error(f"Error unloading plugin '{plugin_name}': {e}")
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
async def reload_plugin(self, plugin_name: str) -> bool:
|
|
502
|
+
"""Reload a plugin (unload and load again)"""
|
|
503
|
+
config = None
|
|
504
|
+
if plugin_name in self.plugins:
|
|
505
|
+
config = self.plugins[plugin_name].config
|
|
506
|
+
await self.unload_plugin(plugin_name)
|
|
507
|
+
|
|
508
|
+
return await self.load_plugin(plugin_name, config)
|
|
509
|
+
|
|
510
|
+
def get_plugin(self, name: str) -> Optional[BasePlugin]:
|
|
511
|
+
"""Get loaded plugin by name"""
|
|
512
|
+
return self.plugins.get(name)
|
|
513
|
+
|
|
514
|
+
def get_all_plugins(self) -> Dict[str, PluginInfo]:
|
|
515
|
+
"""Get information about all plugins"""
|
|
516
|
+
# Include discovered but not loaded plugins
|
|
517
|
+
discovered = {p.name: p for p in self.discover_plugins()}
|
|
518
|
+
discovered.update(self.plugin_info)
|
|
519
|
+
return discovered
|
|
520
|
+
|
|
521
|
+
def get_plugins_by_type(self, plugin_type: PluginType) -> List[BasePlugin]:
|
|
522
|
+
"""Get all plugins of a specific type"""
|
|
523
|
+
return [
|
|
524
|
+
p for p in self.plugins.values()
|
|
525
|
+
if p.PLUGIN_TYPE == plugin_type
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
async def execute_plugin(self, name: str, **kwargs) -> Any:
|
|
529
|
+
"""
|
|
530
|
+
Execute a plugin by name.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
name: Plugin name
|
|
534
|
+
**kwargs: Arguments to pass to plugin
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
Plugin execution result
|
|
538
|
+
"""
|
|
539
|
+
plugin = self.get_plugin(name)
|
|
540
|
+
if not plugin:
|
|
541
|
+
raise ValueError(f"Plugin '{name}' not found or not loaded")
|
|
542
|
+
|
|
543
|
+
return await plugin.execute(**kwargs)
|
|
544
|
+
|
|
545
|
+
async def load_all_plugins(self):
|
|
546
|
+
"""Load all discovered plugins"""
|
|
547
|
+
discovered = self.discover_plugins()
|
|
548
|
+
|
|
549
|
+
for info in discovered:
|
|
550
|
+
if info.name not in self.plugins:
|
|
551
|
+
await self.load_plugin(info.name)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# Global plugin manager instance
|
|
555
|
+
plugin_manager = PluginManager()
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# Convenience functions
|
|
559
|
+
def register_hook(hook_name: str, callback: Callable, priority: int = 10):
|
|
560
|
+
"""Register a hook callback"""
|
|
561
|
+
plugin_manager.hooks.register_hook(hook_name, callback, priority)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def apply_filter(filter_name: str, data: Any, **kwargs):
|
|
565
|
+
"""Apply filters to data"""
|
|
566
|
+
return plugin_manager.hooks.apply_filter(filter_name, data, **kwargs)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
async def execute_hook(hook_name: str, *args, **kwargs):
|
|
570
|
+
"""Execute a hook"""
|
|
571
|
+
return await plugin_manager.hooks.execute_hook(hook_name, *args, **kwargs)
|