crackerjack 0.32.0__py3-none-any.whl → 0.33.1__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +64 -6
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +257 -218
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +558 -240
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +66 -13
- crackerjack/managers/test_command_builder.py +5 -17
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +109 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +161 -32
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +174 -33
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +15 -12
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +3 -0
- crackerjack/mixins/error_handling.py +145 -0
- crackerjack/models/config.py +21 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +176 -107
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/models/task.py +3 -0
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +90 -105
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +18 -11
- crackerjack/services/config_merge.py +30 -85
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +41 -17
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +41 -45
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +30 -33
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +409 -0
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +416 -0
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +250 -576
- crackerjack/services/performance_cache.py +382 -0
- crackerjack/services/performance_monitor.py +565 -0
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +605 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +61 -30
- crackerjack/services/security_logger.py +18 -22
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/terminal_utils.py +0 -0
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
- crackerjack-0.33.1.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.32.0.dist-info/RECORD +0 -180
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,17 +12,15 @@ from .security_logger import get_security_logger
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def find_mcp_server_processes() -> list[dict[str, t.Any]]:
|
|
15
|
-
"""Find running MCP server processes using secure subprocess execution."""
|
|
16
15
|
security_logger = get_security_logger()
|
|
17
16
|
|
|
18
17
|
try:
|
|
19
|
-
# Use secure subprocess execution with validation
|
|
20
18
|
result = execute_secure_subprocess(
|
|
21
19
|
command=["ps", "aux"],
|
|
22
20
|
capture_output=True,
|
|
23
21
|
text=True,
|
|
24
22
|
check=True,
|
|
25
|
-
timeout=10.0,
|
|
23
|
+
timeout=10.0,
|
|
26
24
|
)
|
|
27
25
|
|
|
28
26
|
return _parse_mcp_processes(result.stdout)
|
|
@@ -37,7 +35,6 @@ def find_mcp_server_processes() -> list[dict[str, t.Any]]:
|
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
def _parse_mcp_processes(stdout: str) -> list[dict[str, t.Any]]:
|
|
40
|
-
"""Parse MCP server processes from ps command output."""
|
|
41
38
|
processes: list[dict[str, t.Any]] = []
|
|
42
39
|
str(Path.cwd())
|
|
43
40
|
|
|
@@ -51,7 +48,6 @@ def _parse_mcp_processes(stdout: str) -> list[dict[str, t.Any]]:
|
|
|
51
48
|
|
|
52
49
|
|
|
53
50
|
def _is_mcp_server_process(line: str) -> bool:
|
|
54
|
-
"""Check if a line represents an MCP server process."""
|
|
55
51
|
return (
|
|
56
52
|
"crackerjack" in line
|
|
57
53
|
and "--start-mcp-server" in line
|
|
@@ -59,15 +55,22 @@ def _is_mcp_server_process(line: str) -> bool:
|
|
|
59
55
|
)
|
|
60
56
|
|
|
61
57
|
|
|
58
|
+
def _is_zuban_lsp_process(line: str) -> bool:
|
|
59
|
+
return (
|
|
60
|
+
"zuban" in line
|
|
61
|
+
and "server" in line
|
|
62
|
+
and ("uv" in line.lower() or "python" in line.lower())
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
62
66
|
def _extract_process_info(line: str) -> dict[str, t.Any] | None:
|
|
63
|
-
"""Extract process information from a ps output line."""
|
|
64
67
|
parts = line.split()
|
|
65
68
|
if len(parts) < 11:
|
|
66
69
|
return None
|
|
67
70
|
|
|
68
71
|
try:
|
|
69
72
|
pid = int(parts[1])
|
|
70
|
-
|
|
73
|
+
|
|
71
74
|
command_start_index = _find_command_start_index(parts)
|
|
72
75
|
|
|
73
76
|
return {
|
|
@@ -82,7 +85,6 @@ def _extract_process_info(line: str) -> dict[str, t.Any] | None:
|
|
|
82
85
|
|
|
83
86
|
|
|
84
87
|
def _find_command_start_index(parts: list[str]) -> int:
|
|
85
|
-
"""Find the index where the command starts in ps output."""
|
|
86
88
|
command_start_index = 10
|
|
87
89
|
for i, part in enumerate(parts):
|
|
88
90
|
if part.endswith("python") or "Python" in part:
|
|
@@ -92,17 +94,15 @@ def _find_command_start_index(parts: list[str]) -> int:
|
|
|
92
94
|
|
|
93
95
|
|
|
94
96
|
def find_websocket_server_processes() -> list[dict[str, t.Any]]:
|
|
95
|
-
"""Find running WebSocket server processes using secure subprocess execution."""
|
|
96
97
|
security_logger = get_security_logger()
|
|
97
98
|
|
|
98
99
|
try:
|
|
99
|
-
# Use secure subprocess execution with validation
|
|
100
100
|
result = execute_secure_subprocess(
|
|
101
101
|
command=["ps", "aux"],
|
|
102
102
|
capture_output=True,
|
|
103
103
|
text=True,
|
|
104
104
|
check=True,
|
|
105
|
-
timeout=10.0,
|
|
105
|
+
timeout=10.0,
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
processes: list[dict[str, t.Any]] = []
|
|
@@ -136,6 +136,43 @@ def find_websocket_server_processes() -> list[dict[str, t.Any]]:
|
|
|
136
136
|
return []
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def find_zuban_lsp_processes() -> list[dict[str, t.Any]]:
|
|
140
|
+
"""Find running zuban LSP server processes."""
|
|
141
|
+
security_logger = get_security_logger()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
result = execute_secure_subprocess(
|
|
145
|
+
command=["ps", "aux"],
|
|
146
|
+
capture_output=True,
|
|
147
|
+
text=True,
|
|
148
|
+
check=True,
|
|
149
|
+
timeout=10.0,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return _parse_zuban_lsp_processes(result.stdout)
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
security_logger.log_subprocess_failure(
|
|
156
|
+
command=["ps", "aux"],
|
|
157
|
+
exit_code=-1,
|
|
158
|
+
error_output=str(e),
|
|
159
|
+
)
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _parse_zuban_lsp_processes(stdout: str) -> list[dict[str, t.Any]]:
|
|
164
|
+
"""Parse zuban LSP processes from ps output."""
|
|
165
|
+
processes: list[dict[str, t.Any]] = []
|
|
166
|
+
|
|
167
|
+
for line in stdout.splitlines():
|
|
168
|
+
if _is_zuban_lsp_process(line):
|
|
169
|
+
process_info = _extract_process_info(line)
|
|
170
|
+
if process_info:
|
|
171
|
+
processes.append(process_info)
|
|
172
|
+
|
|
173
|
+
return processes
|
|
174
|
+
|
|
175
|
+
|
|
139
176
|
def stop_process(pid: int, force: bool = False) -> bool:
|
|
140
177
|
try:
|
|
141
178
|
if force:
|
|
@@ -204,14 +241,38 @@ def stop_websocket_server(console: Console | None = None) -> bool:
|
|
|
204
241
|
return success
|
|
205
242
|
|
|
206
243
|
|
|
244
|
+
def stop_zuban_lsp(console: Console | None = None) -> bool:
|
|
245
|
+
"""Stop running zuban LSP server processes."""
|
|
246
|
+
if console is None:
|
|
247
|
+
console = Console()
|
|
248
|
+
|
|
249
|
+
processes = find_zuban_lsp_processes()
|
|
250
|
+
|
|
251
|
+
if not processes:
|
|
252
|
+
console.print("[yellow]⚠️ No Zuban LSP server processes found[/ yellow]")
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
success = True
|
|
256
|
+
for proc in processes:
|
|
257
|
+
console.print(f"🛑 Stopping Zuban LSP server process {proc['pid']}")
|
|
258
|
+
if stop_process(proc["pid"]):
|
|
259
|
+
console.print(f"✅ Stopped process {proc['pid']}")
|
|
260
|
+
else:
|
|
261
|
+
console.print(f"❌ Failed to stop process {proc['pid']}")
|
|
262
|
+
success = False
|
|
263
|
+
|
|
264
|
+
return success
|
|
265
|
+
|
|
266
|
+
|
|
207
267
|
def stop_all_servers(console: Console | None = None) -> bool:
|
|
208
268
|
if console is None:
|
|
209
269
|
console = Console()
|
|
210
270
|
|
|
211
271
|
mcp_success = stop_mcp_server(console)
|
|
212
272
|
websocket_success = stop_websocket_server(console)
|
|
273
|
+
zuban_lsp_success = stop_zuban_lsp(console)
|
|
213
274
|
|
|
214
|
-
return mcp_success and websocket_success
|
|
275
|
+
return mcp_success and websocket_success and zuban_lsp_success
|
|
215
276
|
|
|
216
277
|
|
|
217
278
|
def restart_mcp_server(
|
|
@@ -230,12 +291,10 @@ def restart_mcp_server(
|
|
|
230
291
|
|
|
231
292
|
console.print("🚀 Starting new MCP server...")
|
|
232
293
|
try:
|
|
233
|
-
# Build command with proper argument formatting
|
|
234
294
|
cmd = [sys.executable, "-m", "crackerjack", "--start-mcp-server"]
|
|
235
295
|
if websocket_port:
|
|
236
296
|
cmd.extend(["--websocket-port", str(websocket_port)])
|
|
237
297
|
|
|
238
|
-
# Use secure subprocess execution for server restart
|
|
239
298
|
import subprocess
|
|
240
299
|
|
|
241
300
|
subprocess.Popen(
|
|
@@ -245,7 +304,6 @@ def restart_mcp_server(
|
|
|
245
304
|
start_new_session=True,
|
|
246
305
|
)
|
|
247
306
|
|
|
248
|
-
# Log the secure server start
|
|
249
307
|
security_logger = get_security_logger()
|
|
250
308
|
security_logger.log_subprocess_execution(
|
|
251
309
|
command=cmd,
|
|
@@ -260,6 +318,45 @@ def restart_mcp_server(
|
|
|
260
318
|
return False
|
|
261
319
|
|
|
262
320
|
|
|
321
|
+
def restart_zuban_lsp(console: Console | None = None) -> bool:
|
|
322
|
+
"""Restart zuban LSP server."""
|
|
323
|
+
if console is None:
|
|
324
|
+
console = Console()
|
|
325
|
+
|
|
326
|
+
console.print("[bold cyan]🔄 Restarting Zuban LSP server...[/ bold cyan]")
|
|
327
|
+
|
|
328
|
+
stop_zuban_lsp(console)
|
|
329
|
+
|
|
330
|
+
console.print("⏳ Waiting for cleanup...")
|
|
331
|
+
time.sleep(2)
|
|
332
|
+
|
|
333
|
+
console.print("🚀 Starting new Zuban LSP server...")
|
|
334
|
+
try:
|
|
335
|
+
cmd = [sys.executable, "-m", "crackerjack", "--start-zuban-lsp"]
|
|
336
|
+
|
|
337
|
+
import subprocess
|
|
338
|
+
|
|
339
|
+
subprocess.Popen(
|
|
340
|
+
cmd,
|
|
341
|
+
stdout=subprocess.DEVNULL,
|
|
342
|
+
stderr=subprocess.DEVNULL,
|
|
343
|
+
start_new_session=True,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
security_logger = get_security_logger()
|
|
347
|
+
security_logger.log_subprocess_execution(
|
|
348
|
+
command=cmd,
|
|
349
|
+
purpose="zuban_lsp_restart",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
console.print("✅ Zuban LSP server restart initiated")
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
except Exception as e:
|
|
356
|
+
console.print(f"❌ Failed to restart Zuban LSP server: {e}")
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
|
|
263
360
|
def list_server_status(console: Console | None = None) -> None:
|
|
264
361
|
if console is None:
|
|
265
362
|
console = Console()
|
|
@@ -268,6 +365,7 @@ def list_server_status(console: Console | None = None) -> None:
|
|
|
268
365
|
|
|
269
366
|
mcp_processes = find_mcp_server_processes()
|
|
270
367
|
websocket_processes = find_websocket_server_processes()
|
|
368
|
+
zuban_lsp_processes = find_zuban_lsp_processes()
|
|
271
369
|
|
|
272
370
|
if mcp_processes:
|
|
273
371
|
console.print("\n[bold green]MCP Servers: [/ bold green]")
|
|
@@ -289,5 +387,15 @@ def list_server_status(console: Console | None = None) -> None:
|
|
|
289
387
|
else:
|
|
290
388
|
console.print("\n[yellow]WebSocket Servers: None running[/ yellow]")
|
|
291
389
|
|
|
292
|
-
if
|
|
390
|
+
if zuban_lsp_processes:
|
|
391
|
+
console.print("\n[bold green]Zuban LSP Servers: [/ bold green]")
|
|
392
|
+
for proc in zuban_lsp_processes:
|
|
393
|
+
console.print(
|
|
394
|
+
f" • PID {proc['pid']} - CPU: {proc['cpu']}%-Memory: {proc['mem']}%",
|
|
395
|
+
)
|
|
396
|
+
console.print(f" Command: {proc['command']}")
|
|
397
|
+
else:
|
|
398
|
+
console.print("\n[yellow]Zuban LSP Servers: None running[/ yellow]")
|
|
399
|
+
|
|
400
|
+
if not mcp_processes and not websocket_processes and not zuban_lsp_processes:
|
|
293
401
|
console.print("\n[dim]No crackerjack servers currently running[/ dim]")
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Status Authentication System for secure access control.
|
|
3
|
-
|
|
4
|
-
Provides authentication and authorization for status endpoints with
|
|
5
|
-
JWT tokens, API keys, role-based access control, and audit logging.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
1
|
import hashlib
|
|
9
2
|
import hmac
|
|
10
3
|
import json
|
|
@@ -18,17 +11,13 @@ from .security_logger import SecurityEventLevel, SecurityEventType, get_security
|
|
|
18
11
|
|
|
19
12
|
|
|
20
13
|
class AccessLevel(str, Enum):
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
ADMIN = "admin" # Full administrative access
|
|
26
|
-
DEBUG = "debug" # Debug-level information
|
|
14
|
+
PUBLIC = "public"
|
|
15
|
+
INTERNAL = "internal"
|
|
16
|
+
ADMIN = "admin"
|
|
17
|
+
DEBUG = "debug"
|
|
27
18
|
|
|
28
19
|
|
|
29
20
|
class AuthenticationMethod(str, Enum):
|
|
30
|
-
"""Supported authentication methods."""
|
|
31
|
-
|
|
32
21
|
API_KEY = "api_key"
|
|
33
22
|
JWT_TOKEN = "jwt_token"
|
|
34
23
|
HMAC_SIGNATURE = "hmac_signature"
|
|
@@ -37,8 +26,6 @@ class AuthenticationMethod(str, Enum):
|
|
|
37
26
|
|
|
38
27
|
@dataclass
|
|
39
28
|
class AuthCredentials:
|
|
40
|
-
"""Authentication credentials for status access."""
|
|
41
|
-
|
|
42
29
|
client_id: str
|
|
43
30
|
access_level: AccessLevel
|
|
44
31
|
method: AuthenticationMethod
|
|
@@ -47,69 +34,39 @@ class AuthCredentials:
|
|
|
47
34
|
|
|
48
35
|
@property
|
|
49
36
|
def is_expired(self) -> bool:
|
|
50
|
-
"""Check if credentials are expired."""
|
|
51
37
|
return self.expires_at is not None and time.time() > self.expires_at
|
|
52
38
|
|
|
53
39
|
def has_operation_access(self, operation: str) -> bool:
|
|
54
|
-
"""Check if credentials allow specific operation."""
|
|
55
40
|
return self.allowed_operations is None or operation in self.allowed_operations
|
|
56
41
|
|
|
57
42
|
|
|
58
43
|
class AuthenticationError(Exception):
|
|
59
|
-
"""Base authentication error."""
|
|
60
|
-
|
|
61
44
|
pass
|
|
62
45
|
|
|
63
46
|
|
|
64
47
|
class AccessDeniedError(AuthenticationError):
|
|
65
|
-
"""Access denied error."""
|
|
66
|
-
|
|
67
48
|
pass
|
|
68
49
|
|
|
69
50
|
|
|
70
51
|
class ExpiredCredentialsError(AuthenticationError):
|
|
71
|
-
"""Expired credentials error."""
|
|
72
|
-
|
|
73
52
|
pass
|
|
74
53
|
|
|
75
54
|
|
|
76
55
|
class StatusAuthenticator:
|
|
77
|
-
"""
|
|
78
|
-
Authentication system for status endpoints.
|
|
79
|
-
|
|
80
|
-
Features:
|
|
81
|
-
- Multiple authentication methods (API keys, JWT, HMAC)
|
|
82
|
-
- Role-based access control
|
|
83
|
-
- Time-based credential expiration
|
|
84
|
-
- Operation-level permissions
|
|
85
|
-
- Comprehensive audit logging
|
|
86
|
-
- Local-only access mode for development
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
56
|
def __init__(
|
|
90
57
|
self,
|
|
91
58
|
secret_key: str | None = None,
|
|
92
59
|
default_access_level: AccessLevel = AccessLevel.PUBLIC,
|
|
93
60
|
enable_local_only: bool = True,
|
|
94
61
|
):
|
|
95
|
-
"""
|
|
96
|
-
Initialize status authenticator.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
secret_key: Secret key for HMAC and JWT validation
|
|
100
|
-
default_access_level: Default access level for unauthenticated requests
|
|
101
|
-
enable_local_only: Allow local-only access for development
|
|
102
|
-
"""
|
|
103
62
|
self.secret_key = secret_key or self._generate_secret_key()
|
|
104
63
|
self.default_access_level = default_access_level
|
|
105
64
|
self.enable_local_only = enable_local_only
|
|
106
65
|
|
|
107
66
|
self.security_logger = get_security_logger()
|
|
108
67
|
|
|
109
|
-
# Valid API keys and their associated credentials
|
|
110
68
|
self._api_keys: dict[str, AuthCredentials] = {}
|
|
111
69
|
|
|
112
|
-
# Access level requirements for operations
|
|
113
70
|
self._operation_requirements: dict[str, AccessLevel] = {
|
|
114
71
|
"get_basic_status": AccessLevel.PUBLIC,
|
|
115
72
|
"get_service_status": AccessLevel.INTERNAL,
|
|
@@ -120,17 +77,12 @@ class StatusAuthenticator:
|
|
|
120
77
|
"clear_cache": AccessLevel.ADMIN,
|
|
121
78
|
}
|
|
122
79
|
|
|
123
|
-
# Initialize with default API keys if none exist
|
|
124
80
|
self._initialize_default_keys()
|
|
125
81
|
|
|
126
82
|
def _generate_secret_key(self) -> str:
|
|
127
|
-
"""Generate a secure random secret key."""
|
|
128
83
|
return secrets.token_urlsafe(32)
|
|
129
84
|
|
|
130
85
|
def _initialize_default_keys(self) -> None:
|
|
131
|
-
"""Initialize with default API keys for different access levels."""
|
|
132
|
-
|
|
133
|
-
# Generate default API keys for different access levels
|
|
134
86
|
default_keys = {
|
|
135
87
|
AccessLevel.PUBLIC: self._generate_api_key("public_default"),
|
|
136
88
|
AccessLevel.INTERNAL: self._generate_api_key("internal_default"),
|
|
@@ -145,7 +97,6 @@ class StatusAuthenticator:
|
|
|
145
97
|
)
|
|
146
98
|
|
|
147
99
|
def _generate_api_key(self, prefix: str = "ck") -> str:
|
|
148
|
-
"""Generate a secure API key with prefix."""
|
|
149
100
|
random_part = secrets.token_urlsafe(32)
|
|
150
101
|
return f"{prefix}_{random_part}"
|
|
151
102
|
|
|
@@ -155,23 +106,6 @@ class StatusAuthenticator:
|
|
|
155
106
|
client_ip: str | None = None,
|
|
156
107
|
operation: str = "unknown",
|
|
157
108
|
) -> AuthCredentials:
|
|
158
|
-
"""
|
|
159
|
-
Authenticate a status request.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
auth_header: Authorization header value
|
|
163
|
-
client_ip: Client IP address
|
|
164
|
-
operation: Operation being requested
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
AuthCredentials for the authenticated request
|
|
168
|
-
|
|
169
|
-
Raises:
|
|
170
|
-
AuthenticationError: If authentication fails
|
|
171
|
-
AccessDeniedError: If access is denied
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
# Check for local-only access (development mode)
|
|
175
109
|
if self.enable_local_only and client_ip:
|
|
176
110
|
if client_ip in ("127.0.0.1", "::1", "localhost"):
|
|
177
111
|
self.security_logger.log_security_event(
|
|
@@ -185,13 +119,11 @@ class StatusAuthenticator:
|
|
|
185
119
|
|
|
186
120
|
return AuthCredentials(
|
|
187
121
|
client_id="local",
|
|
188
|
-
access_level=AccessLevel.ADMIN,
|
|
122
|
+
access_level=AccessLevel.ADMIN,
|
|
189
123
|
method=AuthenticationMethod.LOCAL_ONLY,
|
|
190
124
|
)
|
|
191
125
|
|
|
192
|
-
# Parse authentication header
|
|
193
126
|
if not auth_header:
|
|
194
|
-
# Return default access level for unauthenticated requests
|
|
195
127
|
credentials = AuthCredentials(
|
|
196
128
|
client_id="anonymous",
|
|
197
129
|
access_level=self.default_access_level,
|
|
@@ -200,13 +132,10 @@ class StatusAuthenticator:
|
|
|
200
132
|
else:
|
|
201
133
|
credentials = self._parse_auth_header(auth_header, operation)
|
|
202
134
|
|
|
203
|
-
# Validate credentials
|
|
204
135
|
self._validate_credentials(credentials, operation)
|
|
205
136
|
|
|
206
|
-
# Check operation permissions
|
|
207
137
|
self._check_operation_access(credentials, operation)
|
|
208
138
|
|
|
209
|
-
# Log successful authentication
|
|
210
139
|
self.security_logger.log_security_event(
|
|
211
140
|
event_type=SecurityEventType.AUTH_SUCCESS,
|
|
212
141
|
level=SecurityEventLevel.INFO,
|
|
@@ -223,27 +152,20 @@ class StatusAuthenticator:
|
|
|
223
152
|
return credentials
|
|
224
153
|
|
|
225
154
|
def _parse_auth_header(self, auth_header: str, operation: str) -> AuthCredentials:
|
|
226
|
-
"""Parse authentication header and create credentials."""
|
|
227
|
-
|
|
228
155
|
try:
|
|
229
|
-
# Handle different authentication schemes
|
|
230
156
|
if auth_header.startswith("Bearer "):
|
|
231
|
-
|
|
232
|
-
token = auth_header[7:] # Remove "Bearer " prefix
|
|
157
|
+
token = auth_header[7:]
|
|
233
158
|
return self._validate_jwt_token(token, operation)
|
|
234
159
|
|
|
235
160
|
elif auth_header.startswith("ApiKey "):
|
|
236
|
-
|
|
237
|
-
api_key = auth_header[7:] # Remove "ApiKey " prefix
|
|
161
|
+
api_key = auth_header[7:]
|
|
238
162
|
return self._validate_api_key(api_key, operation)
|
|
239
163
|
|
|
240
164
|
elif auth_header.startswith("HMAC-SHA256 "):
|
|
241
|
-
|
|
242
|
-
signature_data = auth_header[12:] # Remove "HMAC-SHA256 " prefix
|
|
165
|
+
signature_data = auth_header[12:]
|
|
243
166
|
return self._validate_hmac_signature(signature_data, operation)
|
|
244
167
|
|
|
245
168
|
else:
|
|
246
|
-
# Try as plain API key
|
|
247
169
|
return self._validate_api_key(auth_header, operation)
|
|
248
170
|
|
|
249
171
|
except Exception as e:
|
|
@@ -256,8 +178,6 @@ class StatusAuthenticator:
|
|
|
256
178
|
raise AuthenticationError(f"Invalid authentication format: {e}")
|
|
257
179
|
|
|
258
180
|
def _validate_api_key(self, api_key: str, operation: str) -> AuthCredentials:
|
|
259
|
-
"""Validate API key and return credentials."""
|
|
260
|
-
|
|
261
181
|
if api_key in self._api_keys:
|
|
262
182
|
credentials = self._api_keys[api_key]
|
|
263
183
|
|
|
@@ -285,22 +205,17 @@ class StatusAuthenticator:
|
|
|
285
205
|
raise AuthenticationError("Invalid API key")
|
|
286
206
|
|
|
287
207
|
def _validate_jwt_token(self, token: str, operation: str) -> AuthCredentials:
|
|
288
|
-
"""Validate JWT token and return credentials."""
|
|
289
|
-
|
|
290
208
|
try:
|
|
291
|
-
# Simple JWT validation (in production, use proper JWT library)
|
|
292
209
|
parts = token.split(".")
|
|
293
210
|
if len(parts) != 3:
|
|
294
211
|
raise AuthenticationError("Invalid JWT format")
|
|
295
212
|
|
|
296
|
-
# Decode header and payload (base64)
|
|
297
213
|
import base64
|
|
298
214
|
|
|
299
215
|
json.loads(base64.b64decode(parts[0] + "=="))
|
|
300
216
|
payload = json.loads(base64.b64decode(parts[1] + "=="))
|
|
301
217
|
signature = parts[2]
|
|
302
218
|
|
|
303
|
-
# Validate signature with HMAC
|
|
304
219
|
expected_signature = (
|
|
305
220
|
base64.b64encode(
|
|
306
221
|
hmac.new(
|
|
@@ -316,11 +231,9 @@ class StatusAuthenticator:
|
|
|
316
231
|
if not hmac.compare_digest(signature, expected_signature):
|
|
317
232
|
raise AuthenticationError("Invalid JWT signature")
|
|
318
233
|
|
|
319
|
-
# Check expiration
|
|
320
234
|
if "exp" in payload and time.time() > payload["exp"]:
|
|
321
235
|
raise ExpiredCredentialsError("JWT token expired")
|
|
322
236
|
|
|
323
|
-
# Extract credentials
|
|
324
237
|
client_id = payload.get("sub", "jwt_user")
|
|
325
238
|
access_level = AccessLevel(
|
|
326
239
|
payload.get("access_level", AccessLevel.PUBLIC.value)
|
|
@@ -332,7 +245,7 @@ class StatusAuthenticator:
|
|
|
332
245
|
access_level=access_level,
|
|
333
246
|
method=AuthenticationMethod.JWT_TOKEN,
|
|
334
247
|
expires_at=payload.get("exp"),
|
|
335
|
-
allowed_operations=set(allowed_operations)
|
|
248
|
+
allowed_operations=set[t.Any](allowed_operations)
|
|
336
249
|
if allowed_operations
|
|
337
250
|
else None,
|
|
338
251
|
)
|
|
@@ -349,24 +262,19 @@ class StatusAuthenticator:
|
|
|
349
262
|
def _validate_hmac_signature(
|
|
350
263
|
self, signature_data: str, operation: str
|
|
351
264
|
) -> AuthCredentials:
|
|
352
|
-
"""Validate HMAC signature and return credentials."""
|
|
353
|
-
|
|
354
265
|
try:
|
|
355
|
-
|
|
356
|
-
parts = signature_data.split(":")
|
|
266
|
+
parts = signature_data.split(": ")
|
|
357
267
|
if len(parts) != 3:
|
|
358
268
|
raise AuthenticationError("Invalid HMAC signature format")
|
|
359
269
|
|
|
360
270
|
client_id, timestamp_str, signature = parts
|
|
361
271
|
timestamp = float(timestamp_str)
|
|
362
272
|
|
|
363
|
-
# Check timestamp (prevent replay attacks)
|
|
364
273
|
current_time = time.time()
|
|
365
|
-
if abs(current_time - timestamp) > 300:
|
|
274
|
+
if abs(current_time - timestamp) > 300:
|
|
366
275
|
raise ExpiredCredentialsError("HMAC signature timestamp too old")
|
|
367
276
|
|
|
368
|
-
|
|
369
|
-
message = f"{client_id}:{operation}:{timestamp_str}"
|
|
277
|
+
message = f"{client_id}: {operation}: {timestamp_str}"
|
|
370
278
|
expected_signature = hmac.new(
|
|
371
279
|
self.secret_key.encode(),
|
|
372
280
|
message.encode(),
|
|
@@ -378,7 +286,7 @@ class StatusAuthenticator:
|
|
|
378
286
|
|
|
379
287
|
return AuthCredentials(
|
|
380
288
|
client_id=client_id,
|
|
381
|
-
access_level=AccessLevel.INTERNAL,
|
|
289
|
+
access_level=AccessLevel.INTERNAL,
|
|
382
290
|
method=AuthenticationMethod.HMAC_SIGNATURE,
|
|
383
291
|
)
|
|
384
292
|
|
|
@@ -394,8 +302,6 @@ class StatusAuthenticator:
|
|
|
394
302
|
def _validate_credentials(
|
|
395
303
|
self, credentials: AuthCredentials, operation: str
|
|
396
304
|
) -> None:
|
|
397
|
-
"""Validate credentials are still valid."""
|
|
398
|
-
|
|
399
305
|
if credentials.is_expired:
|
|
400
306
|
self.security_logger.log_security_event(
|
|
401
307
|
event_type=SecurityEventType.AUTH_EXPIRED,
|
|
@@ -409,9 +315,6 @@ class StatusAuthenticator:
|
|
|
409
315
|
def _check_operation_access(
|
|
410
316
|
self, credentials: AuthCredentials, operation: str
|
|
411
317
|
) -> None:
|
|
412
|
-
"""Check if credentials have access to the requested operation."""
|
|
413
|
-
|
|
414
|
-
# Check operation-specific permissions
|
|
415
318
|
if not credentials.has_operation_access(operation):
|
|
416
319
|
self.security_logger.log_security_event(
|
|
417
320
|
event_type=SecurityEventType.ACCESS_DENIED,
|
|
@@ -421,12 +324,13 @@ class StatusAuthenticator:
|
|
|
421
324
|
operation=operation,
|
|
422
325
|
additional_data={
|
|
423
326
|
"access_level": credentials.access_level.value,
|
|
424
|
-
"allowed_operations": list
|
|
327
|
+
"allowed_operations": list[t.Any](
|
|
328
|
+
credentials.allowed_operations or []
|
|
329
|
+
),
|
|
425
330
|
},
|
|
426
331
|
)
|
|
427
332
|
raise AccessDeniedError(f"Operation not allowed: {operation}")
|
|
428
333
|
|
|
429
|
-
# Check access level requirements
|
|
430
334
|
required_level = self._operation_requirements.get(operation, AccessLevel.PUBLIC)
|
|
431
335
|
if not self._has_sufficient_access_level(
|
|
432
336
|
credentials.access_level, required_level
|
|
@@ -449,9 +353,6 @@ class StatusAuthenticator:
|
|
|
449
353
|
user_level: AccessLevel,
|
|
450
354
|
required_level: AccessLevel,
|
|
451
355
|
) -> bool:
|
|
452
|
-
"""Check if user access level meets requirements."""
|
|
453
|
-
|
|
454
|
-
# Access level hierarchy
|
|
455
356
|
level_hierarchy = {
|
|
456
357
|
AccessLevel.PUBLIC: 0,
|
|
457
358
|
AccessLevel.INTERNAL: 1,
|
|
@@ -465,7 +366,6 @@ class StatusAuthenticator:
|
|
|
465
366
|
return user_level_num >= required_level_num
|
|
466
367
|
|
|
467
368
|
def is_operation_allowed(self, operation: str, access_level: AccessLevel) -> bool:
|
|
468
|
-
"""Check if an operation is allowed for the given access level."""
|
|
469
369
|
required_level = self._operation_requirements.get(operation, AccessLevel.PUBLIC)
|
|
470
370
|
return self._has_sufficient_access_level(access_level, required_level)
|
|
471
371
|
|
|
@@ -476,19 +376,6 @@ class StatusAuthenticator:
|
|
|
476
376
|
expires_at: float | None = None,
|
|
477
377
|
allowed_operations: set[str] | None = None,
|
|
478
378
|
) -> str:
|
|
479
|
-
"""
|
|
480
|
-
Add a new API key.
|
|
481
|
-
|
|
482
|
-
Args:
|
|
483
|
-
client_id: Client identifier
|
|
484
|
-
access_level: Access level for the key
|
|
485
|
-
expires_at: Optional expiration timestamp
|
|
486
|
-
allowed_operations: Optional set of allowed operations
|
|
487
|
-
|
|
488
|
-
Returns:
|
|
489
|
-
Generated API key
|
|
490
|
-
"""
|
|
491
|
-
|
|
492
379
|
api_key = self._generate_api_key(client_id.replace(" ", "_").lower())
|
|
493
380
|
|
|
494
381
|
credentials = AuthCredentials(
|
|
@@ -516,16 +403,6 @@ class StatusAuthenticator:
|
|
|
516
403
|
return api_key
|
|
517
404
|
|
|
518
405
|
def revoke_api_key(self, api_key: str) -> bool:
|
|
519
|
-
"""
|
|
520
|
-
Revoke an API key.
|
|
521
|
-
|
|
522
|
-
Args:
|
|
523
|
-
api_key: API key to revoke
|
|
524
|
-
|
|
525
|
-
Returns:
|
|
526
|
-
True if key was revoked, False if not found
|
|
527
|
-
"""
|
|
528
|
-
|
|
529
406
|
if api_key in self._api_keys:
|
|
530
407
|
credentials = self._api_keys[api_key]
|
|
531
408
|
del self._api_keys[api_key]
|
|
@@ -543,8 +420,6 @@ class StatusAuthenticator:
|
|
|
543
420
|
return False
|
|
544
421
|
|
|
545
422
|
def get_auth_status(self) -> dict[str, t.Any]:
|
|
546
|
-
"""Get current authentication system status."""
|
|
547
|
-
|
|
548
423
|
active_keys = len([k for k, c in self._api_keys.items() if not c.is_expired])
|
|
549
424
|
expired_keys = len([k for k, c in self._api_keys.items() if c.is_expired])
|
|
550
425
|
|
|
@@ -565,13 +440,10 @@ class StatusAuthenticator:
|
|
|
565
440
|
}
|
|
566
441
|
|
|
567
442
|
|
|
568
|
-
# Global singleton instance
|
|
569
443
|
_authenticator: StatusAuthenticator | None = None
|
|
570
444
|
|
|
571
445
|
|
|
572
446
|
def get_status_authenticator() -> StatusAuthenticator:
|
|
573
|
-
"""Get the global status authenticator instance."""
|
|
574
|
-
|
|
575
447
|
global _authenticator
|
|
576
448
|
if _authenticator is None:
|
|
577
449
|
_authenticator = StatusAuthenticator()
|
|
@@ -583,21 +455,6 @@ async def authenticate_status_request(
|
|
|
583
455
|
client_ip: str | None = None,
|
|
584
456
|
operation: str = "unknown",
|
|
585
457
|
) -> AuthCredentials:
|
|
586
|
-
"""
|
|
587
|
-
Convenience function for status request authentication.
|
|
588
|
-
|
|
589
|
-
Args:
|
|
590
|
-
auth_header: Authorization header value
|
|
591
|
-
client_ip: Client IP address
|
|
592
|
-
operation: Operation being requested
|
|
593
|
-
|
|
594
|
-
Returns:
|
|
595
|
-
AuthCredentials for the authenticated request
|
|
596
|
-
|
|
597
|
-
Raises:
|
|
598
|
-
AuthenticationError: If authentication fails
|
|
599
|
-
"""
|
|
600
|
-
|
|
601
458
|
return get_status_authenticator().authenticate_request(
|
|
602
459
|
auth_header, client_ip, operation
|
|
603
460
|
)
|