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.

Files changed (198) hide show
  1. crackerjack/__main__.py +1350 -34
  2. crackerjack/adapters/__init__.py +17 -0
  3. crackerjack/adapters/lsp_client.py +358 -0
  4. crackerjack/adapters/rust_tool_adapter.py +194 -0
  5. crackerjack/adapters/rust_tool_manager.py +193 -0
  6. crackerjack/adapters/skylos_adapter.py +231 -0
  7. crackerjack/adapters/zuban_adapter.py +560 -0
  8. crackerjack/agents/base.py +7 -3
  9. crackerjack/agents/coordinator.py +271 -33
  10. crackerjack/agents/documentation_agent.py +9 -15
  11. crackerjack/agents/dry_agent.py +3 -15
  12. crackerjack/agents/formatting_agent.py +1 -1
  13. crackerjack/agents/import_optimization_agent.py +36 -180
  14. crackerjack/agents/performance_agent.py +17 -98
  15. crackerjack/agents/performance_helpers.py +7 -31
  16. crackerjack/agents/proactive_agent.py +1 -3
  17. crackerjack/agents/refactoring_agent.py +16 -85
  18. crackerjack/agents/refactoring_helpers.py +7 -42
  19. crackerjack/agents/security_agent.py +9 -48
  20. crackerjack/agents/test_creation_agent.py +356 -513
  21. crackerjack/agents/test_specialist_agent.py +0 -4
  22. crackerjack/api.py +6 -25
  23. crackerjack/cli/cache_handlers.py +204 -0
  24. crackerjack/cli/cache_handlers_enhanced.py +683 -0
  25. crackerjack/cli/facade.py +100 -0
  26. crackerjack/cli/handlers.py +224 -9
  27. crackerjack/cli/interactive.py +6 -4
  28. crackerjack/cli/options.py +642 -55
  29. crackerjack/cli/utils.py +2 -1
  30. crackerjack/code_cleaner.py +58 -117
  31. crackerjack/config/global_lock_config.py +8 -48
  32. crackerjack/config/hooks.py +53 -62
  33. crackerjack/core/async_workflow_orchestrator.py +24 -34
  34. crackerjack/core/autofix_coordinator.py +3 -17
  35. crackerjack/core/enhanced_container.py +4 -13
  36. crackerjack/core/file_lifecycle.py +12 -89
  37. crackerjack/core/performance.py +2 -2
  38. crackerjack/core/performance_monitor.py +15 -55
  39. crackerjack/core/phase_coordinator.py +104 -204
  40. crackerjack/core/resource_manager.py +14 -90
  41. crackerjack/core/service_watchdog.py +62 -95
  42. crackerjack/core/session_coordinator.py +149 -0
  43. crackerjack/core/timeout_manager.py +14 -72
  44. crackerjack/core/websocket_lifecycle.py +13 -78
  45. crackerjack/core/workflow_orchestrator.py +171 -174
  46. crackerjack/docs/INDEX.md +11 -0
  47. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  48. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  49. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  50. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  51. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  52. crackerjack/documentation/__init__.py +31 -0
  53. crackerjack/documentation/ai_templates.py +756 -0
  54. crackerjack/documentation/dual_output_generator.py +765 -0
  55. crackerjack/documentation/mkdocs_integration.py +518 -0
  56. crackerjack/documentation/reference_generator.py +977 -0
  57. crackerjack/dynamic_config.py +55 -50
  58. crackerjack/executors/async_hook_executor.py +10 -15
  59. crackerjack/executors/cached_hook_executor.py +117 -43
  60. crackerjack/executors/hook_executor.py +8 -34
  61. crackerjack/executors/hook_lock_manager.py +26 -183
  62. crackerjack/executors/individual_hook_executor.py +13 -11
  63. crackerjack/executors/lsp_aware_hook_executor.py +270 -0
  64. crackerjack/executors/tool_proxy.py +417 -0
  65. crackerjack/hooks/lsp_hook.py +79 -0
  66. crackerjack/intelligence/adaptive_learning.py +25 -10
  67. crackerjack/intelligence/agent_orchestrator.py +2 -5
  68. crackerjack/intelligence/agent_registry.py +34 -24
  69. crackerjack/intelligence/agent_selector.py +5 -7
  70. crackerjack/interactive.py +17 -6
  71. crackerjack/managers/async_hook_manager.py +0 -1
  72. crackerjack/managers/hook_manager.py +79 -1
  73. crackerjack/managers/publish_manager.py +44 -8
  74. crackerjack/managers/test_command_builder.py +1 -15
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +98 -7
  77. crackerjack/managers/test_manager_backup.py +10 -9
  78. crackerjack/mcp/cache.py +2 -2
  79. crackerjack/mcp/client_runner.py +1 -1
  80. crackerjack/mcp/context.py +191 -68
  81. crackerjack/mcp/dashboard.py +7 -5
  82. crackerjack/mcp/enhanced_progress_monitor.py +31 -28
  83. crackerjack/mcp/file_monitor.py +30 -23
  84. crackerjack/mcp/progress_components.py +31 -21
  85. crackerjack/mcp/progress_monitor.py +50 -53
  86. crackerjack/mcp/rate_limiter.py +6 -6
  87. crackerjack/mcp/server_core.py +17 -16
  88. crackerjack/mcp/service_watchdog.py +2 -1
  89. crackerjack/mcp/state.py +4 -7
  90. crackerjack/mcp/task_manager.py +11 -9
  91. crackerjack/mcp/tools/core_tools.py +173 -32
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +8 -10
  94. crackerjack/mcp/tools/execution_tools_backup.py +42 -30
  95. crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
  96. crackerjack/mcp/tools/intelligence_tools.py +5 -2
  97. crackerjack/mcp/tools/monitoring_tools.py +33 -70
  98. crackerjack/mcp/tools/proactive_tools.py +24 -11
  99. crackerjack/mcp/tools/progress_tools.py +5 -8
  100. crackerjack/mcp/tools/utility_tools.py +20 -14
  101. crackerjack/mcp/tools/workflow_executor.py +62 -40
  102. crackerjack/mcp/websocket/app.py +8 -0
  103. crackerjack/mcp/websocket/endpoints.py +352 -357
  104. crackerjack/mcp/websocket/jobs.py +40 -57
  105. crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
  106. crackerjack/mcp/websocket/server.py +7 -25
  107. crackerjack/mcp/websocket/websocket_handler.py +6 -17
  108. crackerjack/mixins/__init__.py +0 -2
  109. crackerjack/mixins/error_handling.py +1 -70
  110. crackerjack/models/config.py +12 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +122 -122
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  115. crackerjack/monitoring/metrics_collector.py +426 -0
  116. crackerjack/monitoring/regression_prevention.py +8 -8
  117. crackerjack/monitoring/websocket_server.py +643 -0
  118. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  119. crackerjack/orchestration/coverage_improvement.py +3 -3
  120. crackerjack/orchestration/execution_strategies.py +26 -6
  121. crackerjack/orchestration/test_progress_streamer.py +8 -5
  122. crackerjack/plugins/base.py +2 -2
  123. crackerjack/plugins/hooks.py +7 -0
  124. crackerjack/plugins/managers.py +11 -8
  125. crackerjack/security/__init__.py +0 -1
  126. crackerjack/security/audit.py +6 -35
  127. crackerjack/services/anomaly_detector.py +392 -0
  128. crackerjack/services/api_extractor.py +615 -0
  129. crackerjack/services/backup_service.py +2 -2
  130. crackerjack/services/bounded_status_operations.py +15 -152
  131. crackerjack/services/cache.py +127 -1
  132. crackerjack/services/changelog_automation.py +395 -0
  133. crackerjack/services/config.py +15 -9
  134. crackerjack/services/config_merge.py +19 -80
  135. crackerjack/services/config_template.py +506 -0
  136. crackerjack/services/contextual_ai_assistant.py +48 -22
  137. crackerjack/services/coverage_badge_service.py +171 -0
  138. crackerjack/services/coverage_ratchet.py +27 -25
  139. crackerjack/services/debug.py +3 -3
  140. crackerjack/services/dependency_analyzer.py +460 -0
  141. crackerjack/services/dependency_monitor.py +14 -11
  142. crackerjack/services/documentation_generator.py +491 -0
  143. crackerjack/services/documentation_service.py +675 -0
  144. crackerjack/services/enhanced_filesystem.py +6 -5
  145. crackerjack/services/enterprise_optimizer.py +865 -0
  146. crackerjack/services/error_pattern_analyzer.py +676 -0
  147. crackerjack/services/file_hasher.py +1 -1
  148. crackerjack/services/git.py +8 -25
  149. crackerjack/services/health_metrics.py +10 -8
  150. crackerjack/services/heatmap_generator.py +735 -0
  151. crackerjack/services/initialization.py +11 -30
  152. crackerjack/services/input_validator.py +5 -97
  153. crackerjack/services/intelligent_commit.py +327 -0
  154. crackerjack/services/log_manager.py +15 -12
  155. crackerjack/services/logging.py +4 -3
  156. crackerjack/services/lsp_client.py +628 -0
  157. crackerjack/services/memory_optimizer.py +19 -87
  158. crackerjack/services/metrics.py +42 -33
  159. crackerjack/services/parallel_executor.py +9 -67
  160. crackerjack/services/pattern_cache.py +1 -1
  161. crackerjack/services/pattern_detector.py +6 -6
  162. crackerjack/services/performance_benchmarks.py +18 -59
  163. crackerjack/services/performance_cache.py +20 -81
  164. crackerjack/services/performance_monitor.py +27 -95
  165. crackerjack/services/predictive_analytics.py +510 -0
  166. crackerjack/services/quality_baseline.py +234 -0
  167. crackerjack/services/quality_baseline_enhanced.py +646 -0
  168. crackerjack/services/quality_intelligence.py +785 -0
  169. crackerjack/services/regex_patterns.py +605 -524
  170. crackerjack/services/regex_utils.py +43 -123
  171. crackerjack/services/secure_path_utils.py +5 -164
  172. crackerjack/services/secure_status_formatter.py +30 -141
  173. crackerjack/services/secure_subprocess.py +11 -92
  174. crackerjack/services/security.py +9 -41
  175. crackerjack/services/security_logger.py +12 -24
  176. crackerjack/services/server_manager.py +124 -16
  177. crackerjack/services/status_authentication.py +16 -159
  178. crackerjack/services/status_security_manager.py +4 -131
  179. crackerjack/services/thread_safe_status_collector.py +19 -125
  180. crackerjack/services/unified_config.py +21 -13
  181. crackerjack/services/validation_rate_limiter.py +5 -54
  182. crackerjack/services/version_analyzer.py +459 -0
  183. crackerjack/services/version_checker.py +1 -1
  184. crackerjack/services/websocket_resource_limiter.py +10 -144
  185. crackerjack/services/zuban_lsp_service.py +390 -0
  186. crackerjack/slash_commands/__init__.py +2 -7
  187. crackerjack/slash_commands/run.md +2 -2
  188. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  189. crackerjack/tools/validate_regex_patterns.py +19 -48
  190. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
  191. crackerjack-0.33.1.dist-info/RECORD +229 -0
  192. crackerjack/CLAUDE.md +0 -207
  193. crackerjack/RULES.md +0 -380
  194. crackerjack/py313.py +0 -234
  195. crackerjack-0.33.0.dist-info/RECORD +0 -187
  196. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  197. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  198. {crackerjack-0.33.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, # 10 second timeout for process listing
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
- # Find where the command actually starts (usually after the time field)
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, # 10 second timeout for process listing
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 not mcp_processes and not websocket_processes:
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
- """Access levels for status endpoints."""
22
-
23
- PUBLIC = "public" # Basic system status
24
- INTERNAL = "internal" # Internal service status
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, # Local gets admin access
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
- # JWT token
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
- # API key authentication
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
- # HMAC signature authentication
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
- # Parse signature data: client_id:timestamp:signature
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: # 5 minute window
274
+ if abs(current_time - timestamp) > 300:
366
275
  raise ExpiredCredentialsError("HMAC signature timestamp too old")
367
276
 
368
- # Create expected signature
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, # HMAC gets internal access
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(credentials.allowed_operations or []),
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
  )