crackerjack 0.33.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 +4 -13
- 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 +104 -204
- 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 +171 -174
- 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 +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -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 +17 -16
- 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 +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- 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 +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- 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 +6 -35
- 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 +15 -9
- crackerjack/services/config_merge.py +19 -80
- 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 +27 -25
- 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 +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- 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 +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- 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 +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- 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.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
- 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.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Thread-safe status collection to prevent race conditions.
|
|
3
|
-
|
|
4
|
-
Provides synchronized status data collection with proper locking,
|
|
5
|
-
atomic operations, and consistency guarantees.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
1
|
import asyncio
|
|
9
2
|
import json
|
|
10
3
|
import threading
|
|
@@ -18,12 +11,10 @@ from .security_logger import SecurityEventLevel, SecurityEventType, get_security
|
|
|
18
11
|
|
|
19
12
|
@dataclass
|
|
20
13
|
class StatusSnapshot:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
server_stats: dict[str, t.Any] = field(default_factory=dict)
|
|
26
|
-
agent_suggestions: dict[str, t.Any] = field(default_factory=dict)
|
|
14
|
+
services: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
15
|
+
jobs: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
16
|
+
server_stats: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
17
|
+
agent_suggestions: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
27
18
|
timestamp: float = field(default_factory=time.time)
|
|
28
19
|
collection_duration: float = 0.0
|
|
29
20
|
is_complete: bool = False
|
|
@@ -31,43 +22,22 @@ class StatusSnapshot:
|
|
|
31
22
|
|
|
32
23
|
|
|
33
24
|
class ThreadSafeStatusCollector:
|
|
34
|
-
|
|
35
|
-
Thread-safe status collector with race condition prevention.
|
|
36
|
-
|
|
37
|
-
Features:
|
|
38
|
-
- Thread-safe data collection and aggregation
|
|
39
|
-
- Atomic status updates with consistency guarantees
|
|
40
|
-
- Deadlock prevention with ordered locking
|
|
41
|
-
- Timeout protection for all operations
|
|
42
|
-
- Error isolation and recovery
|
|
43
|
-
"""
|
|
44
|
-
|
|
45
|
-
def __init__(self, timeout: float = 30.0):
|
|
46
|
-
"""
|
|
47
|
-
Initialize thread-safe status collector.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
timeout: Maximum time for status collection operations
|
|
51
|
-
"""
|
|
25
|
+
def __init__(self, timeout: float = 30.0) -> None:
|
|
52
26
|
self.timeout = timeout
|
|
53
27
|
self.security_logger = get_security_logger()
|
|
54
28
|
|
|
55
|
-
|
|
56
|
-
self.
|
|
57
|
-
self.
|
|
58
|
-
self._file_lock = threading.RLock() # File operation lock
|
|
29
|
+
self._collection_lock = threading.RLock()
|
|
30
|
+
self._data_lock = threading.RLock()
|
|
31
|
+
self._file_lock = threading.RLock()
|
|
59
32
|
|
|
60
|
-
# Status collection state
|
|
61
33
|
self._current_snapshot: StatusSnapshot | None = None
|
|
62
34
|
self._collection_in_progress = False
|
|
63
35
|
self._collection_start_time = 0.0
|
|
64
36
|
|
|
65
|
-
# Cached data with expiration
|
|
66
37
|
self._cache: dict[str, t.Any] = {}
|
|
67
38
|
self._cache_timestamps: dict[str, float] = {}
|
|
68
|
-
self._cache_ttl = 5.0
|
|
39
|
+
self._cache_ttl = 5.0
|
|
69
40
|
|
|
70
|
-
# Thread-local storage for per-thread state
|
|
71
41
|
self._local = threading.local()
|
|
72
42
|
|
|
73
43
|
async def collect_comprehensive_status(
|
|
@@ -77,32 +47,12 @@ class ThreadSafeStatusCollector:
|
|
|
77
47
|
include_services: bool = True,
|
|
78
48
|
include_stats: bool = True,
|
|
79
49
|
) -> StatusSnapshot:
|
|
80
|
-
"""
|
|
81
|
-
Collect comprehensive system status with thread safety.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
client_id: Client identifier for logging
|
|
85
|
-
include_jobs: Include job information
|
|
86
|
-
include_services: Include service information
|
|
87
|
-
include_stats: Include server statistics
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
StatusSnapshot with collected data
|
|
91
|
-
|
|
92
|
-
Raises:
|
|
93
|
-
TimeoutError: If collection takes too long
|
|
94
|
-
RuntimeError: If collection fails due to concurrency issues
|
|
95
|
-
"""
|
|
96
|
-
|
|
97
|
-
# Use async context manager for proper cleanup
|
|
98
50
|
async with self._collection_context(client_id):
|
|
99
51
|
start_time = time.time()
|
|
100
52
|
|
|
101
53
|
try:
|
|
102
|
-
# Create new snapshot
|
|
103
54
|
snapshot = StatusSnapshot(timestamp=start_time)
|
|
104
55
|
|
|
105
|
-
# Collect data components in parallel with timeouts
|
|
106
56
|
collection_tasks = []
|
|
107
57
|
|
|
108
58
|
if include_services:
|
|
@@ -120,21 +70,18 @@ class ThreadSafeStatusCollector:
|
|
|
120
70
|
self._collect_server_stats(client_id, snapshot)
|
|
121
71
|
)
|
|
122
72
|
|
|
123
|
-
# Wait for all collections to complete with timeout
|
|
124
73
|
await asyncio.wait_for(
|
|
125
74
|
asyncio.gather(*collection_tasks, return_exceptions=True),
|
|
126
75
|
timeout=self.timeout,
|
|
127
76
|
)
|
|
128
77
|
|
|
129
|
-
# Finalize snapshot
|
|
130
78
|
snapshot.collection_duration = time.time() - start_time
|
|
131
79
|
snapshot.is_complete = True
|
|
132
80
|
|
|
133
|
-
# Log successful collection
|
|
134
81
|
self.security_logger.log_security_event(
|
|
135
82
|
event_type=SecurityEventType.STATUS_COLLECTED,
|
|
136
83
|
level=SecurityEventLevel.INFO,
|
|
137
|
-
message=f"Status collection completed in {snapshot.collection_duration
|
|
84
|
+
message=f"Status collection completed in {snapshot.collection_duration: .2f}s",
|
|
138
85
|
client_id=client_id,
|
|
139
86
|
operation="collect_status",
|
|
140
87
|
additional_data={
|
|
@@ -169,7 +116,6 @@ class ThreadSafeStatusCollector:
|
|
|
169
116
|
additional_data={"error": str(e)},
|
|
170
117
|
)
|
|
171
118
|
|
|
172
|
-
# Return partial snapshot with error information
|
|
173
119
|
snapshot = StatusSnapshot(
|
|
174
120
|
timestamp=start_time,
|
|
175
121
|
collection_duration=time.time() - start_time,
|
|
@@ -180,16 +126,12 @@ class ThreadSafeStatusCollector:
|
|
|
180
126
|
return snapshot
|
|
181
127
|
|
|
182
128
|
@asynccontextmanager
|
|
183
|
-
async def _collection_context(self, client_id: str):
|
|
184
|
-
"""Context manager for status collection with proper cleanup."""
|
|
185
|
-
|
|
186
|
-
# Acquire collection lock with timeout
|
|
129
|
+
async def _collection_context(self, client_id: str) -> t.AsyncGenerator[None]:
|
|
187
130
|
collection_acquired = False
|
|
188
131
|
start_wait = time.time()
|
|
189
132
|
|
|
190
133
|
try:
|
|
191
|
-
|
|
192
|
-
while time.time() - start_wait < 5.0: # 5 second wait limit
|
|
134
|
+
while time.time() - start_wait < 5.0:
|
|
193
135
|
with self._collection_lock:
|
|
194
136
|
if not self._collection_in_progress:
|
|
195
137
|
self._collection_in_progress = True
|
|
@@ -213,7 +155,6 @@ class ThreadSafeStatusCollector:
|
|
|
213
155
|
yield
|
|
214
156
|
|
|
215
157
|
finally:
|
|
216
|
-
# Always release the collection lock
|
|
217
158
|
if collection_acquired:
|
|
218
159
|
with self._collection_lock:
|
|
219
160
|
self._collection_in_progress = False
|
|
@@ -232,23 +173,18 @@ class ThreadSafeStatusCollector:
|
|
|
232
173
|
client_id: str,
|
|
233
174
|
snapshot: StatusSnapshot,
|
|
234
175
|
) -> None:
|
|
235
|
-
"""Collect services data with thread safety."""
|
|
236
|
-
|
|
237
176
|
try:
|
|
238
|
-
# Check cache first
|
|
239
177
|
cached_data = self._get_cached_data("services")
|
|
240
178
|
if cached_data is not None:
|
|
241
179
|
with self._data_lock:
|
|
242
180
|
snapshot.services = cached_data
|
|
243
181
|
return
|
|
244
182
|
|
|
245
|
-
# Import here to avoid circular dependencies
|
|
246
183
|
from crackerjack.services.server_manager import (
|
|
247
184
|
find_mcp_server_processes,
|
|
248
185
|
find_websocket_server_processes,
|
|
249
186
|
)
|
|
250
187
|
|
|
251
|
-
# Collect process data with timeout
|
|
252
188
|
mcp_task = asyncio.create_task(asyncio.to_thread(find_mcp_server_processes))
|
|
253
189
|
websocket_task = asyncio.create_task(
|
|
254
190
|
asyncio.to_thread(find_websocket_server_processes)
|
|
@@ -270,7 +206,6 @@ class ThreadSafeStatusCollector:
|
|
|
270
206
|
},
|
|
271
207
|
}
|
|
272
208
|
|
|
273
|
-
# Atomically update snapshot and cache
|
|
274
209
|
with self._data_lock:
|
|
275
210
|
snapshot.services = services_data
|
|
276
211
|
self._set_cached_data("services", services_data)
|
|
@@ -286,17 +221,13 @@ class ThreadSafeStatusCollector:
|
|
|
286
221
|
client_id: str,
|
|
287
222
|
snapshot: StatusSnapshot,
|
|
288
223
|
) -> None:
|
|
289
|
-
"""Collect jobs data with thread safety."""
|
|
290
|
-
|
|
291
224
|
try:
|
|
292
|
-
# Check cache first
|
|
293
225
|
cached_data = self._get_cached_data("jobs")
|
|
294
226
|
if cached_data is not None:
|
|
295
227
|
with self._data_lock:
|
|
296
228
|
snapshot.jobs = cached_data
|
|
297
229
|
return
|
|
298
230
|
|
|
299
|
-
# Get active jobs with file locking
|
|
300
231
|
active_jobs = await self._get_active_jobs_safe()
|
|
301
232
|
|
|
302
233
|
jobs_data = {
|
|
@@ -312,7 +243,6 @@ class ThreadSafeStatusCollector:
|
|
|
312
243
|
"details": active_jobs,
|
|
313
244
|
}
|
|
314
245
|
|
|
315
|
-
# Atomically update snapshot and cache
|
|
316
246
|
with self._data_lock:
|
|
317
247
|
snapshot.jobs = jobs_data
|
|
318
248
|
self._set_cached_data("jobs", jobs_data)
|
|
@@ -328,14 +258,11 @@ class ThreadSafeStatusCollector:
|
|
|
328
258
|
client_id: str,
|
|
329
259
|
snapshot: StatusSnapshot,
|
|
330
260
|
) -> None:
|
|
331
|
-
"""Collect server statistics with thread safety."""
|
|
332
|
-
|
|
333
261
|
try:
|
|
334
|
-
# Get context safely
|
|
335
262
|
from crackerjack.mcp.context import get_context
|
|
336
263
|
|
|
337
264
|
try:
|
|
338
|
-
context = get_context()
|
|
265
|
+
context: MCPServerContext | None = get_context()
|
|
339
266
|
except RuntimeError:
|
|
340
267
|
context = None
|
|
341
268
|
|
|
@@ -344,14 +271,12 @@ class ThreadSafeStatusCollector:
|
|
|
344
271
|
snapshot.server_stats = {"error": "Server context not available"}
|
|
345
272
|
return
|
|
346
273
|
|
|
347
|
-
# Build stats with timeout protection
|
|
348
274
|
stats_task = asyncio.create_task(
|
|
349
275
|
asyncio.to_thread(self._build_server_stats_safe, context)
|
|
350
276
|
)
|
|
351
277
|
|
|
352
278
|
server_stats = await asyncio.wait_for(stats_task, timeout=5.0)
|
|
353
279
|
|
|
354
|
-
# Atomically update snapshot
|
|
355
280
|
with self._data_lock:
|
|
356
281
|
snapshot.server_stats = server_stats
|
|
357
282
|
|
|
@@ -362,11 +287,8 @@ class ThreadSafeStatusCollector:
|
|
|
362
287
|
snapshot.server_stats = {"error": error_msg}
|
|
363
288
|
|
|
364
289
|
async def _get_active_jobs_safe(self) -> list[dict[str, t.Any]]:
|
|
365
|
-
|
|
290
|
+
jobs: list[dict[str, t.Any]] = []
|
|
366
291
|
|
|
367
|
-
jobs = []
|
|
368
|
-
|
|
369
|
-
# Use file lock to prevent race conditions during file reading
|
|
370
292
|
with self._file_lock:
|
|
371
293
|
try:
|
|
372
294
|
from crackerjack.mcp.context import get_context
|
|
@@ -375,14 +297,11 @@ class ThreadSafeStatusCollector:
|
|
|
375
297
|
if not context or not context.progress_dir.exists():
|
|
376
298
|
return jobs
|
|
377
299
|
|
|
378
|
-
# Read job files with error handling
|
|
379
300
|
for progress_file in context.progress_dir.glob("job-*.json"):
|
|
380
301
|
try:
|
|
381
|
-
# Use atomic read with timeout
|
|
382
302
|
content = progress_file.read_text(encoding="utf-8")
|
|
383
303
|
progress_data = json.loads(content)
|
|
384
304
|
|
|
385
|
-
# Validate required fields
|
|
386
305
|
job_data = {
|
|
387
306
|
"job_id": progress_data.get("job_id", "unknown"),
|
|
388
307
|
"status": progress_data.get("status", "unknown"),
|
|
@@ -403,7 +322,6 @@ class ThreadSafeStatusCollector:
|
|
|
403
322
|
jobs.append(job_data)
|
|
404
323
|
|
|
405
324
|
except (json.JSONDecodeError, OSError, UnicodeDecodeError) as e:
|
|
406
|
-
# Log file read error but continue processing other files
|
|
407
325
|
self.security_logger.log_security_event(
|
|
408
326
|
event_type=SecurityEventType.FILE_READ_ERROR,
|
|
409
327
|
level=SecurityEventLevel.WARNING,
|
|
@@ -423,8 +341,6 @@ class ThreadSafeStatusCollector:
|
|
|
423
341
|
return jobs
|
|
424
342
|
|
|
425
343
|
def _build_server_stats_safe(self, context: t.Any) -> dict[str, t.Any]:
|
|
426
|
-
"""Build server stats with thread safety."""
|
|
427
|
-
|
|
428
344
|
try:
|
|
429
345
|
stats = {
|
|
430
346
|
"server_info": {
|
|
@@ -442,7 +358,9 @@ class ThreadSafeStatusCollector:
|
|
|
442
358
|
else None,
|
|
443
359
|
},
|
|
444
360
|
"resource_usage": {
|
|
445
|
-
"temp_files_count": len(
|
|
361
|
+
"temp_files_count": len(
|
|
362
|
+
list[t.Any](context.progress_dir.glob("*.json"))
|
|
363
|
+
)
|
|
446
364
|
if context.progress_dir.exists()
|
|
447
365
|
else 0,
|
|
448
366
|
"progress_dir": str(context.progress_dir),
|
|
@@ -450,7 +368,6 @@ class ThreadSafeStatusCollector:
|
|
|
450
368
|
"timestamp": time.time(),
|
|
451
369
|
}
|
|
452
370
|
|
|
453
|
-
# Add state manager stats if available
|
|
454
371
|
state_manager = getattr(context, "state_manager", None)
|
|
455
372
|
if state_manager:
|
|
456
373
|
stats["state_manager"] = {
|
|
@@ -465,35 +382,28 @@ class ThreadSafeStatusCollector:
|
|
|
465
382
|
return {"error": f"Failed to build server stats: {e}"}
|
|
466
383
|
|
|
467
384
|
def _get_cached_data(self, key: str) -> dict[str, t.Any] | None:
|
|
468
|
-
"""Get cached data if still valid."""
|
|
469
|
-
|
|
470
385
|
current_time = time.time()
|
|
471
386
|
|
|
472
387
|
with self._data_lock:
|
|
473
388
|
if key in self._cache and key in self._cache_timestamps:
|
|
474
389
|
cache_age = current_time - self._cache_timestamps[key]
|
|
475
390
|
if cache_age < self._cache_ttl:
|
|
476
|
-
|
|
391
|
+
cached_result: dict[str, t.Any] = self._cache[key]
|
|
392
|
+
return cached_result
|
|
477
393
|
|
|
478
394
|
return None
|
|
479
395
|
|
|
480
396
|
def _set_cached_data(self, key: str, data: dict[str, t.Any]) -> None:
|
|
481
|
-
"""Set cached data with timestamp."""
|
|
482
|
-
|
|
483
397
|
with self._data_lock:
|
|
484
398
|
self._cache[key] = data.copy() if hasattr(data, "copy") else data
|
|
485
399
|
self._cache_timestamps[key] = time.time()
|
|
486
400
|
|
|
487
401
|
def clear_cache(self) -> None:
|
|
488
|
-
"""Clear all cached data."""
|
|
489
|
-
|
|
490
402
|
with self._data_lock:
|
|
491
403
|
self._cache.clear()
|
|
492
404
|
self._cache_timestamps.clear()
|
|
493
405
|
|
|
494
406
|
def get_collection_status(self) -> dict[str, t.Any]:
|
|
495
|
-
"""Get current collection status and metrics."""
|
|
496
|
-
|
|
497
407
|
with self._collection_lock:
|
|
498
408
|
return {
|
|
499
409
|
"collection_in_progress": self._collection_in_progress,
|
|
@@ -505,13 +415,10 @@ class ThreadSafeStatusCollector:
|
|
|
505
415
|
}
|
|
506
416
|
|
|
507
417
|
|
|
508
|
-
# Global singleton instance
|
|
509
418
|
_status_collector: ThreadSafeStatusCollector | None = None
|
|
510
419
|
|
|
511
420
|
|
|
512
421
|
def get_thread_safe_status_collector() -> ThreadSafeStatusCollector:
|
|
513
|
-
"""Get the global thread-safe status collector instance."""
|
|
514
|
-
|
|
515
422
|
global _status_collector
|
|
516
423
|
if _status_collector is None:
|
|
517
424
|
_status_collector = ThreadSafeStatusCollector()
|
|
@@ -524,19 +431,6 @@ async def collect_secure_status(
|
|
|
524
431
|
include_services: bool = True,
|
|
525
432
|
include_stats: bool = True,
|
|
526
433
|
) -> StatusSnapshot:
|
|
527
|
-
"""
|
|
528
|
-
Convenience function for secure status collection.
|
|
529
|
-
|
|
530
|
-
Args:
|
|
531
|
-
client_id: Client identifier for logging
|
|
532
|
-
include_jobs: Include job information
|
|
533
|
-
include_services: Include service information
|
|
534
|
-
include_stats: Include server statistics
|
|
535
|
-
|
|
536
|
-
Returns:
|
|
537
|
-
StatusSnapshot with collected data
|
|
538
|
-
"""
|
|
539
|
-
|
|
540
434
|
collector = get_thread_safe_status_collector()
|
|
541
435
|
return await collector.collect_comprehensive_status(
|
|
542
436
|
client_id=client_id,
|
|
@@ -36,8 +36,6 @@ class CrackerjackConfig(BaseModel):
|
|
|
36
36
|
skip_hooks: bool = False
|
|
37
37
|
experimental_hooks: bool = False
|
|
38
38
|
|
|
39
|
-
# Removed unused configuration fields: performance_tracking, benchmark_mode, publish_enabled, keyring_provider
|
|
40
|
-
|
|
41
39
|
batch_file_operations: bool = True
|
|
42
40
|
file_operation_batch_size: int = 10
|
|
43
41
|
|
|
@@ -48,7 +46,8 @@ class CrackerjackConfig(BaseModel):
|
|
|
48
46
|
def validate_package_path(cls, v: Any) -> Path:
|
|
49
47
|
if isinstance(v, str):
|
|
50
48
|
v = Path(v)
|
|
51
|
-
|
|
49
|
+
resolved_path: Path = v.resolve()
|
|
50
|
+
return resolved_path
|
|
52
51
|
|
|
53
52
|
@field_validator("log_file", mode="before")
|
|
54
53
|
@classmethod
|
|
@@ -56,8 +55,10 @@ class CrackerjackConfig(BaseModel):
|
|
|
56
55
|
if v is None:
|
|
57
56
|
return v
|
|
58
57
|
if isinstance(v, str):
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
path_v: Path = Path(v)
|
|
59
|
+
return path_v
|
|
60
|
+
validated_v: Path = t.cast(Path, v)
|
|
61
|
+
return validated_v
|
|
61
62
|
|
|
62
63
|
@field_validator("test_workers")
|
|
63
64
|
@classmethod
|
|
@@ -119,7 +120,6 @@ class EnvironmentConfigSource(ConfigSource):
|
|
|
119
120
|
if value.lower() in ("false", "0", "no", "off"):
|
|
120
121
|
return False
|
|
121
122
|
|
|
122
|
-
# Handle negative numbers with spaces (e.g., "- 10")
|
|
123
123
|
cleaned_value = value.replace(" ", "")
|
|
124
124
|
|
|
125
125
|
with suppress(ValueError):
|
|
@@ -186,9 +186,13 @@ class PyprojectConfigSource(ConfigSource):
|
|
|
186
186
|
with self.pyproject_path.open("rb") as f:
|
|
187
187
|
pyproject_data = tomllib.load(f)
|
|
188
188
|
|
|
189
|
-
config = pyproject_data.get("tool", {}).get(
|
|
189
|
+
config: dict[str, t.Any] = pyproject_data.get("tool", {}).get(
|
|
190
|
+
"crackerjack", {}
|
|
191
|
+
)
|
|
190
192
|
|
|
191
|
-
self.logger.debug(
|
|
193
|
+
self.logger.debug(
|
|
194
|
+
"Loaded pyproject config", keys=list[t.Any](config.keys())
|
|
195
|
+
)
|
|
192
196
|
return config
|
|
193
197
|
|
|
194
198
|
except ImportError:
|
|
@@ -197,9 +201,13 @@ class PyprojectConfigSource(ConfigSource):
|
|
|
197
201
|
|
|
198
202
|
with self.pyproject_path.open("rb") as f:
|
|
199
203
|
pyproject_data = tomllib.load(f)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
config_data: dict[str, t.Any] = pyproject_data.get("tool", {}).get(
|
|
205
|
+
"crackerjack", {}
|
|
206
|
+
)
|
|
207
|
+
self.logger.debug(
|
|
208
|
+
"Loaded pyproject config", keys=list[t.Any](config_data.keys())
|
|
209
|
+
)
|
|
210
|
+
return config_data
|
|
203
211
|
except ImportError:
|
|
204
212
|
self.logger.warning(
|
|
205
213
|
"No TOML library available for pyproject.toml parsing",
|
|
@@ -236,7 +244,7 @@ class OptionsConfigSource(ConfigSource):
|
|
|
236
244
|
if value is not None:
|
|
237
245
|
config[config_key] = value
|
|
238
246
|
|
|
239
|
-
self.logger.debug("Loaded options config", keys=list(config.keys()))
|
|
247
|
+
self.logger.debug("Loaded options config", keys=list[t.Any](config.keys()))
|
|
240
248
|
return config
|
|
241
249
|
|
|
242
250
|
|
|
@@ -304,7 +312,7 @@ class UnifiedConfigurationService:
|
|
|
304
312
|
"Merged config from source",
|
|
305
313
|
source_type=type(source).__name__,
|
|
306
314
|
priority=source.priority,
|
|
307
|
-
keys=list(source_config.keys()),
|
|
315
|
+
keys=list[t.Any](source_config.keys()),
|
|
308
316
|
)
|
|
309
317
|
except Exception as e:
|
|
310
318
|
self.logger.exception(
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Rate limiting for input validation to prevent abuse and DoS attacks.
|
|
3
|
-
|
|
4
|
-
This module provides rate limiting functionality to:
|
|
5
|
-
- Prevent excessive validation failures
|
|
6
|
-
- Protect against DoS attacks via malformed input
|
|
7
|
-
- Track validation patterns for security monitoring
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
1
|
import time
|
|
11
2
|
import typing as t
|
|
12
3
|
from collections import defaultdict, deque
|
|
@@ -16,13 +7,11 @@ from .security_logger import SecurityEventLevel, get_security_logger
|
|
|
16
7
|
|
|
17
8
|
|
|
18
9
|
class ValidationRateLimit:
|
|
19
|
-
"""Rate limiting configuration for different validation types."""
|
|
20
|
-
|
|
21
10
|
def __init__(
|
|
22
11
|
self,
|
|
23
12
|
max_failures: int = 10,
|
|
24
13
|
window_seconds: int = 60,
|
|
25
|
-
block_duration: int = 300,
|
|
14
|
+
block_duration: int = 300,
|
|
26
15
|
):
|
|
27
16
|
self.max_failures = max_failures
|
|
28
17
|
self.window_seconds = window_seconds
|
|
@@ -30,20 +19,12 @@ class ValidationRateLimit:
|
|
|
30
19
|
|
|
31
20
|
|
|
32
21
|
class ValidationRateLimiter:
|
|
33
|
-
|
|
34
|
-
Rate limiter for validation failures to prevent abuse.
|
|
35
|
-
|
|
36
|
-
Tracks validation failures by client/source and blocks excessive failures.
|
|
37
|
-
Uses sliding window approach for accurate rate limiting.
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
def __init__(self):
|
|
22
|
+
def __init__(self) -> None:
|
|
41
23
|
self._failure_windows: dict[str, deque[float]] = defaultdict(deque)
|
|
42
24
|
self._blocked_until: dict[str, float] = {}
|
|
43
25
|
self._lock = Lock()
|
|
44
26
|
self._logger = get_security_logger()
|
|
45
27
|
|
|
46
|
-
# Default rate limits by validation type
|
|
47
28
|
self._limits = {
|
|
48
29
|
"default": ValidationRateLimit(),
|
|
49
30
|
"command_injection": ValidationRateLimit(
|
|
@@ -58,13 +39,11 @@ class ValidationRateLimiter:
|
|
|
58
39
|
}
|
|
59
40
|
|
|
60
41
|
def is_blocked(self, client_id: str) -> bool:
|
|
61
|
-
"""Check if a client is currently blocked."""
|
|
62
42
|
with self._lock:
|
|
63
43
|
if client_id in self._blocked_until:
|
|
64
44
|
if time.time() < self._blocked_until[client_id]:
|
|
65
45
|
return True
|
|
66
46
|
else:
|
|
67
|
-
# Block expired, remove it
|
|
68
47
|
del self._blocked_until[client_id]
|
|
69
48
|
return False
|
|
70
49
|
|
|
@@ -74,35 +53,23 @@ class ValidationRateLimiter:
|
|
|
74
53
|
validation_type: str,
|
|
75
54
|
severity: SecurityEventLevel = SecurityEventLevel.MEDIUM,
|
|
76
55
|
) -> bool:
|
|
77
|
-
"""
|
|
78
|
-
Record a validation failure and check if client should be blocked.
|
|
79
|
-
|
|
80
|
-
Returns True if the client should be blocked, False otherwise.
|
|
81
|
-
"""
|
|
82
56
|
with self._lock:
|
|
83
57
|
current_time = time.time()
|
|
84
58
|
|
|
85
|
-
# Get rate limit for this validation type
|
|
86
59
|
limit = self._limits.get(validation_type, self._limits["default"])
|
|
87
60
|
|
|
88
|
-
# Initialize failure window if needed
|
|
89
61
|
if client_id not in self._failure_windows:
|
|
90
62
|
self._failure_windows[client_id] = deque()
|
|
91
63
|
|
|
92
|
-
# Clean old failures outside the window
|
|
93
64
|
window = self._failure_windows[client_id]
|
|
94
65
|
while window and current_time - window[0] > limit.window_seconds:
|
|
95
66
|
window.popleft()
|
|
96
67
|
|
|
97
|
-
# Record this failure
|
|
98
68
|
window.append(current_time)
|
|
99
69
|
|
|
100
|
-
# Check if limit exceeded
|
|
101
70
|
if len(window) >= limit.max_failures:
|
|
102
|
-
# Block this client
|
|
103
71
|
self._blocked_until[client_id] = current_time + limit.block_duration
|
|
104
72
|
|
|
105
|
-
# Log the rate limit exceeded event
|
|
106
73
|
self._logger.log_rate_limit_exceeded(
|
|
107
74
|
limit_type=validation_type,
|
|
108
75
|
current_count=len(window),
|
|
@@ -111,7 +78,6 @@ class ValidationRateLimiter:
|
|
|
111
78
|
block_duration=limit.block_duration,
|
|
112
79
|
)
|
|
113
80
|
|
|
114
|
-
# Clear the failure window since client is now blocked
|
|
115
81
|
self._failure_windows[client_id].clear()
|
|
116
82
|
|
|
117
83
|
return True
|
|
@@ -119,7 +85,6 @@ class ValidationRateLimiter:
|
|
|
119
85
|
return False
|
|
120
86
|
|
|
121
87
|
def get_remaining_attempts(self, client_id: str, validation_type: str) -> int:
|
|
122
|
-
"""Get remaining validation attempts before rate limit."""
|
|
123
88
|
with self._lock:
|
|
124
89
|
if self.is_blocked(client_id):
|
|
125
90
|
return 0
|
|
@@ -129,7 +94,6 @@ class ValidationRateLimiter:
|
|
|
129
94
|
return max(0, limit.max_failures - current_failures)
|
|
130
95
|
|
|
131
96
|
def get_block_time_remaining(self, client_id: str) -> int:
|
|
132
|
-
"""Get seconds remaining until client is unblocked."""
|
|
133
97
|
with self._lock:
|
|
134
98
|
if client_id in self._blocked_until:
|
|
135
99
|
remaining = max(0, int(self._blocked_until[client_id] - time.time()))
|
|
@@ -137,7 +101,6 @@ class ValidationRateLimiter:
|
|
|
137
101
|
return 0
|
|
138
102
|
|
|
139
103
|
def get_client_stats(self, client_id: str) -> dict[str, t.Any]:
|
|
140
|
-
"""Get rate limiting statistics for a client."""
|
|
141
104
|
with self._lock:
|
|
142
105
|
current_time = time.time()
|
|
143
106
|
|
|
@@ -153,7 +116,6 @@ class ValidationRateLimiter:
|
|
|
153
116
|
window = self._failure_windows[client_id]
|
|
154
117
|
stats["total_failures"] = len(window)
|
|
155
118
|
|
|
156
|
-
# Count recent failures (last 5 minutes)
|
|
157
119
|
recent_count = sum(
|
|
158
120
|
1 for failure_time in window if current_time - failure_time <= 300
|
|
159
121
|
)
|
|
@@ -162,12 +124,10 @@ class ValidationRateLimiter:
|
|
|
162
124
|
return stats
|
|
163
125
|
|
|
164
126
|
def cleanup_expired_data(self) -> int:
|
|
165
|
-
"""Clean up expired data and return number of items removed."""
|
|
166
127
|
with self._lock:
|
|
167
128
|
current_time = time.time()
|
|
168
129
|
removed_count = 0
|
|
169
130
|
|
|
170
|
-
# Clean expired blocks
|
|
171
131
|
expired_blocks = [
|
|
172
132
|
client_id
|
|
173
133
|
for client_id, block_until in self._blocked_until.items()
|
|
@@ -178,14 +138,11 @@ class ValidationRateLimiter:
|
|
|
178
138
|
del self._blocked_until[client_id]
|
|
179
139
|
removed_count += 1
|
|
180
140
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
# Remove failures older than 24 hours
|
|
184
|
-
while window and current_time - window[0] > 86400: # 24 hours
|
|
141
|
+
for client_id, window in list[t.Any](self._failure_windows.items()):
|
|
142
|
+
while window and current_time - window[0] > 86400:
|
|
185
143
|
window.popleft()
|
|
186
144
|
removed_count += 1
|
|
187
145
|
|
|
188
|
-
# Remove empty windows
|
|
189
146
|
if not window:
|
|
190
147
|
del self._failure_windows[client_id]
|
|
191
148
|
|
|
@@ -198,7 +155,6 @@ class ValidationRateLimiter:
|
|
|
198
155
|
window_seconds: int,
|
|
199
156
|
block_duration: int,
|
|
200
157
|
) -> None:
|
|
201
|
-
"""Update rate limits for a specific validation type."""
|
|
202
158
|
with self._lock:
|
|
203
159
|
self._limits[validation_type] = ValidationRateLimit(
|
|
204
160
|
max_failures=max_failures,
|
|
@@ -207,7 +163,6 @@ class ValidationRateLimiter:
|
|
|
207
163
|
)
|
|
208
164
|
|
|
209
165
|
def get_all_stats(self) -> dict[str, t.Any]:
|
|
210
|
-
"""Get comprehensive rate limiting statistics."""
|
|
211
166
|
with self._lock:
|
|
212
167
|
current_time = time.time()
|
|
213
168
|
|
|
@@ -226,7 +181,6 @@ class ValidationRateLimiter:
|
|
|
226
181
|
"active_clients": [],
|
|
227
182
|
}
|
|
228
183
|
|
|
229
|
-
# Get blocked client info
|
|
230
184
|
for client_id, block_until in self._blocked_until.items():
|
|
231
185
|
remaining = max(0, int(block_until - current_time))
|
|
232
186
|
stats["blocked_clients"].append(
|
|
@@ -237,13 +191,12 @@ class ValidationRateLimiter:
|
|
|
237
191
|
}
|
|
238
192
|
)
|
|
239
193
|
|
|
240
|
-
# Get active client info (with recent failures)
|
|
241
194
|
for client_id, window in self._failure_windows.items():
|
|
242
195
|
if client_id not in self._blocked_until and window:
|
|
243
196
|
recent_failures = sum(
|
|
244
197
|
1
|
|
245
198
|
for failure_time in window
|
|
246
|
-
if current_time - failure_time <= 300
|
|
199
|
+
if current_time - failure_time <= 300
|
|
247
200
|
)
|
|
248
201
|
if recent_failures > 0:
|
|
249
202
|
stats["active_clients"].append(
|
|
@@ -257,12 +210,10 @@ class ValidationRateLimiter:
|
|
|
257
210
|
return stats
|
|
258
211
|
|
|
259
212
|
|
|
260
|
-
# Global rate limiter instance
|
|
261
213
|
_rate_limiter: ValidationRateLimiter | None = None
|
|
262
214
|
|
|
263
215
|
|
|
264
216
|
def get_validation_rate_limiter() -> ValidationRateLimiter:
|
|
265
|
-
"""Get the global validation rate limiter instance."""
|
|
266
217
|
global _rate_limiter
|
|
267
218
|
if _rate_limiter is None:
|
|
268
219
|
_rate_limiter = ValidationRateLimiter()
|