crackerjack 0.31.9__py3-none-any.whl → 0.31.12__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 (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +282 -95
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +355 -204
  41. crackerjack/dynamic_config.py +47 -6
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +52 -62
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +51 -76
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +78 -44
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +281 -433
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.12.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.9.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,534 @@
1
+ """
2
+ Secure status formatter to prevent information disclosure vulnerabilities.
3
+
4
+ This module provides secure sanitization of status responses to prevent
5
+ leaking sensitive system information such as absolute paths, internal URLs,
6
+ configuration details, and other sensitive data.
7
+ """
8
+
9
+ import tempfile
10
+ import typing as t
11
+ from enum import Enum
12
+ from pathlib import Path
13
+
14
+ from .security_logger import get_security_logger
15
+
16
+
17
+ class StatusVerbosity(str, Enum):
18
+ """Status output verbosity levels for security-aware responses."""
19
+
20
+ MINIMAL = "minimal" # Only essential operational status
21
+ STANDARD = "standard" # Standard operational information (default)
22
+ DETAILED = "detailed" # More detailed information for debugging
23
+ FULL = "full" # Complete information (for internal use only)
24
+
25
+
26
+ class SecureStatusFormatter:
27
+ """
28
+ Secure status formatter with configurable verbosity levels.
29
+
30
+ Sanitizes sensitive information while preserving necessary operational data.
31
+ """
32
+
33
+ # Patterns for sensitive information that should be masked or removed
34
+ SENSITIVE_PATTERNS = {
35
+ # Absolute system paths (replace with relative paths)
36
+ "absolute_paths": [
37
+ r"(/[^/\s]*){2,}", # Paths like /Users/username/...
38
+ rf"{tempfile.gettempdir()}/[^\s]*", # Temp directory paths - using tempfile.gettempdir() (B108)
39
+ r"/var/[^\s]*", # Var directory paths
40
+ r"/home/[^\s]*", # Home directory paths
41
+ ],
42
+ # URLs with localhost/internal IPs
43
+ "internal_urls": [
44
+ r"https?://localhost:\d+",
45
+ r"https?://127\.0\.0\.1:\d+",
46
+ r"https?://0\.0\.0\.0:\d+",
47
+ r"ws://localhost:\d+",
48
+ r"ws://127\.0\.0\.1:\d+",
49
+ ],
50
+ # Tokens and API keys
51
+ "secrets": [
52
+ r"[A-Za-z0-9]{20,}", # Long alphanumeric strings (potential tokens)
53
+ r"[A-Za-z0-9+/]{32,}={0,2}", # Base64-like strings
54
+ ],
55
+ # Process IDs and system identifiers
56
+ "system_ids": [
57
+ r"pid:\d+",
58
+ r"process_id:\s*\d+",
59
+ ],
60
+ }
61
+
62
+ # Sensitive keys that should be masked or removed
63
+ SENSITIVE_KEYS = {
64
+ "remove_minimal": {
65
+ "progress_dir",
66
+ "temp_files_count",
67
+ "rate_limiter",
68
+ "config",
69
+ "traceback",
70
+ "processes",
71
+ },
72
+ "remove_standard": {"progress_dir", "traceback"},
73
+ "mask": {
74
+ "project_path",
75
+ "websocket_url",
76
+ "monitor_url",
77
+ "api_key",
78
+ "token",
79
+ "secret",
80
+ "password",
81
+ "auth",
82
+ },
83
+ }
84
+
85
+ def __init__(self, project_root: Path | None = None):
86
+ """
87
+ Initialize secure status formatter.
88
+
89
+ Args:
90
+ project_root: Project root path for relative path conversion
91
+ """
92
+ self.project_root = project_root or Path.cwd()
93
+ self.security_logger = get_security_logger()
94
+
95
+ def format_status(
96
+ self,
97
+ status_data: dict[str, t.Any],
98
+ verbosity: StatusVerbosity = StatusVerbosity.STANDARD,
99
+ user_context: str | None = None,
100
+ ) -> dict[str, t.Any]:
101
+ """
102
+ Format status data with security sanitization.
103
+
104
+ Args:
105
+ status_data: Raw status data to sanitize
106
+ verbosity: Output verbosity level
107
+ user_context: Optional user context for logging
108
+
109
+ Returns:
110
+ Sanitized status data appropriate for the verbosity level
111
+ """
112
+ self._log_status_access(status_data, verbosity, user_context)
113
+ sanitized = self._prepare_data_for_sanitization(status_data)
114
+ sanitized = self._apply_all_sanitization_steps(sanitized, verbosity)
115
+ return self._add_security_metadata(sanitized, verbosity)
116
+
117
+ def _log_status_access(
118
+ self,
119
+ status_data: dict[str, t.Any],
120
+ verbosity: StatusVerbosity,
121
+ user_context: str | None,
122
+ ) -> None:
123
+ """Log status access attempt for security monitoring."""
124
+ self.security_logger.log_status_access_attempt(
125
+ endpoint="status_data",
126
+ verbosity_level=verbosity.value,
127
+ user_context=user_context,
128
+ data_keys=list(status_data.keys()),
129
+ )
130
+
131
+ def _prepare_data_for_sanitization(
132
+ self, status_data: dict[str, t.Any]
133
+ ) -> dict[str, t.Any]:
134
+ """Create a deep copy of status data to avoid modifying original."""
135
+ return self._deep_copy_dict(status_data)
136
+
137
+ def _apply_all_sanitization_steps(
138
+ self, data: dict[str, t.Any], verbosity: StatusVerbosity
139
+ ) -> dict[str, t.Any]:
140
+ """Apply all sanitization steps in proper order."""
141
+ data = self._apply_verbosity_filter(data, verbosity)
142
+ return self._sanitize_sensitive_data(data, verbosity)
143
+
144
+ def _add_security_metadata(
145
+ self, data: dict[str, t.Any], verbosity: StatusVerbosity
146
+ ) -> dict[str, t.Any]:
147
+ """Add security metadata to sanitized response."""
148
+ data["_security"] = {
149
+ "sanitized": True,
150
+ "verbosity": verbosity.value,
151
+ "timestamp": self._get_timestamp(),
152
+ }
153
+ return data
154
+
155
+ def _apply_verbosity_filter(
156
+ self, data: dict[str, t.Any], verbosity: StatusVerbosity
157
+ ) -> dict[str, t.Any]:
158
+ """Apply verbosity-based key filtering."""
159
+
160
+ # Keys to remove based on verbosity level
161
+ if verbosity == StatusVerbosity.MINIMAL:
162
+ remove_keys = self.SENSITIVE_KEYS["remove_minimal"]
163
+ elif verbosity == StatusVerbosity.STANDARD:
164
+ remove_keys = self.SENSITIVE_KEYS["remove_standard"]
165
+ else:
166
+ remove_keys = set() # Keep all keys for DETAILED/FULL
167
+
168
+ # Remove sensitive keys
169
+ for key in list(data.keys()):
170
+ if key in remove_keys:
171
+ del data[key]
172
+
173
+ # Recursively apply to nested dictionaries
174
+ for key, value in data.items():
175
+ if isinstance(value, dict):
176
+ data[key] = self._apply_verbosity_filter(value, verbosity)
177
+
178
+ return data
179
+
180
+ def _sanitize_sensitive_data(
181
+ self, data: dict[str, t.Any], verbosity: StatusVerbosity
182
+ ) -> dict[str, t.Any]:
183
+ """Sanitize sensitive data based on patterns and keys."""
184
+
185
+ return self._sanitize_recursive(data, verbosity)
186
+
187
+ def _sanitize_recursive(self, obj: t.Any, verbosity: StatusVerbosity) -> t.Any:
188
+ """Recursively sanitize data structures."""
189
+
190
+ if isinstance(obj, dict):
191
+ sanitized = {}
192
+ for key, value in obj.items():
193
+ # Sanitize the key
194
+ sanitized_key = self._sanitize_string(key, verbosity)
195
+
196
+ # Sanitize the value
197
+ if (
198
+ key in self.SENSITIVE_KEYS["mask"]
199
+ and verbosity != StatusVerbosity.FULL
200
+ ):
201
+ sanitized[sanitized_key] = self._mask_sensitive_value(str(value))
202
+ else:
203
+ sanitized[sanitized_key] = self._sanitize_recursive(
204
+ value, verbosity
205
+ )
206
+
207
+ return sanitized
208
+
209
+ elif isinstance(obj, list):
210
+ return [self._sanitize_recursive(item, verbosity) for item in obj]
211
+
212
+ elif isinstance(obj, str):
213
+ return self._sanitize_string(obj, verbosity)
214
+
215
+ return obj
216
+
217
+ def _sanitize_string(self, text: str, verbosity: StatusVerbosity) -> str:
218
+ """Sanitize string content based on verbosity level."""
219
+ if verbosity == StatusVerbosity.FULL:
220
+ return text # No sanitization for full verbosity
221
+
222
+ return self._apply_string_sanitization_pipeline(text, verbosity)
223
+
224
+ def _apply_string_sanitization_pipeline(
225
+ self, text: str, verbosity: StatusVerbosity
226
+ ) -> str:
227
+ """Apply string sanitization steps in correct order."""
228
+ sanitized = self._sanitize_internal_urls(text)
229
+ sanitized = self._sanitize_paths(sanitized)
230
+ return self._apply_secret_masking_if_needed(sanitized, verbosity)
231
+
232
+ def _apply_secret_masking_if_needed(
233
+ self, text: str, verbosity: StatusVerbosity
234
+ ) -> str:
235
+ """Apply secret masking for minimal verbosity only."""
236
+ if verbosity == StatusVerbosity.MINIMAL:
237
+ return self._mask_potential_secrets(text)
238
+ return text
239
+
240
+ def _sanitize_paths(self, text: str) -> str:
241
+ """Convert absolute paths to relative paths where possible."""
242
+ from .regex_patterns import SAFE_PATTERNS
243
+
244
+ # Use validated patterns for path detection
245
+ unix_path_pattern = SAFE_PATTERNS.get("detect_absolute_unix_paths")
246
+
247
+ if not unix_path_pattern:
248
+ return self._sanitize_paths_fallback(text)
249
+
250
+ return text
251
+
252
+ def _sanitize_paths_fallback(self, text: str) -> str:
253
+ """Fallback path sanitization when patterns don't exist."""
254
+ path_patterns = [
255
+ r"/[a-zA-Z0-9_\-\.\/]+", # Unix-style absolute paths
256
+ r"[A-Z]:[\\\/][a-zA-Z0-9_\-\.\\\/]+", # Windows-style absolute paths
257
+ ]
258
+
259
+ for pattern_str in path_patterns:
260
+ text = self._process_path_pattern(text, pattern_str)
261
+
262
+ return text
263
+
264
+ def _process_path_pattern(self, text: str, pattern_str: str) -> str:
265
+ """Process a single path pattern safely."""
266
+ from contextlib import suppress
267
+
268
+ from .regex_patterns import CompiledPatternCache
269
+
270
+ with suppress(Exception):
271
+ compiled = CompiledPatternCache.get_compiled_pattern(pattern_str)
272
+ matches = compiled.findall(text)
273
+
274
+ for match in matches:
275
+ if len(match) > 3: # Only process paths longer than 3 chars
276
+ text = self._replace_path_match(text, match)
277
+
278
+ return text
279
+
280
+ def _replace_path_match(self, text: str, match: str) -> str:
281
+ """Replace a single path match with relative or redacted version."""
282
+ try:
283
+ abs_path = Path(match)
284
+ if abs_path.is_absolute():
285
+ return self._convert_to_relative_or_redact(text, match, abs_path)
286
+ except (ValueError, OSError):
287
+ # If path operations fail, mask it
288
+ text = text.replace(match, "[REDACTED_PATH]")
289
+
290
+ return text
291
+
292
+ def _convert_to_relative_or_redact(
293
+ self, text: str, match: str, abs_path: Path
294
+ ) -> str:
295
+ """Convert absolute path to relative or redact if outside project."""
296
+ try:
297
+ rel_path = abs_path.relative_to(self.project_root)
298
+ return text.replace(match, f"./{rel_path}")
299
+ except (ValueError, OSError):
300
+ # If not within project, mask it
301
+ return text.replace(match, "[REDACTED_PATH]")
302
+
303
+ def _sanitize_internal_urls(self, text: str) -> str:
304
+ """Replace internal URLs with generic placeholders."""
305
+ from .regex_patterns import sanitize_internal_urls
306
+
307
+ return sanitize_internal_urls(text)
308
+
309
+ def _mask_potential_secrets(self, text: str) -> str:
310
+ """Mask strings that might be secrets."""
311
+ if self._should_skip_secret_masking(text):
312
+ return text
313
+
314
+ patterns_to_check = self._get_validated_secret_patterns()
315
+
316
+ if patterns_to_check:
317
+ return self._apply_validated_secret_patterns(text, patterns_to_check)
318
+ return self._apply_fallback_secret_patterns(text)
319
+
320
+ def _should_skip_secret_masking(self, text: str) -> bool:
321
+ """Check if secret masking should be skipped for already sanitized content."""
322
+ return "[INTERNAL_URL]" in text or "[REDACTED_PATH]" in text
323
+
324
+ def _get_validated_secret_patterns(self) -> list[t.Any]:
325
+ """Get validated patterns from the safe patterns registry."""
326
+ from .regex_patterns import SAFE_PATTERNS
327
+
328
+ patterns = []
329
+ long_alphanumeric = SAFE_PATTERNS.get("detect_long_alphanumeric_tokens")
330
+ base64_like = SAFE_PATTERNS.get("detect_base64_like_strings")
331
+
332
+ if long_alphanumeric:
333
+ patterns.append(long_alphanumeric)
334
+ if base64_like:
335
+ patterns.append(base64_like)
336
+
337
+ return patterns
338
+
339
+ def _apply_validated_secret_patterns(self, text: str, patterns: list[t.Any]) -> str:
340
+ """Apply validated patterns for secret detection."""
341
+ for pattern in patterns:
342
+ try:
343
+ text = self._mask_pattern_matches(text, pattern.findall(text))
344
+ except Exception:
345
+ continue # Skip failed pattern matching
346
+ return text
347
+
348
+ def _apply_fallback_secret_patterns(self, text: str) -> str:
349
+ """Apply fallback patterns when validated patterns don't exist."""
350
+ for pattern_str in self.SENSITIVE_PATTERNS["secrets"]:
351
+ try:
352
+ from .regex_patterns import CompiledPatternCache
353
+
354
+ compiled = CompiledPatternCache.get_compiled_pattern(pattern_str)
355
+ text = self._mask_pattern_matches(text, compiled.findall(text))
356
+ except Exception:
357
+ continue # Skip failed pattern compilation
358
+ return text
359
+
360
+ def _mask_pattern_matches(self, text: str, matches: list[str]) -> str:
361
+ """Mask matches from pattern matching."""
362
+ for match in matches:
363
+ if self._should_mask_match(match):
364
+ masked = self._create_masked_string(match)
365
+ text = text.replace(match, masked)
366
+ return text
367
+
368
+ def _should_mask_match(self, match: str) -> bool:
369
+ """Determine if a match should be masked."""
370
+ if len(match) <= 16:
371
+ return False
372
+ # Don't mask if it looks like a URL or path component
373
+ return not any(x in match for x in ("://", "/", "\\", "."))
374
+
375
+ def _create_masked_string(self, text: str) -> str:
376
+ """Create a masked version of the text."""
377
+ return text[:4] + "*" * (len(text) - 8) + text[-4:]
378
+
379
+ def _mask_sensitive_value(self, value: str) -> str:
380
+ """Mask a known sensitive value."""
381
+
382
+ if len(value) <= 4:
383
+ return "***"
384
+ elif len(value) <= 8:
385
+ return value[0] + "*" * (len(value) - 1)
386
+ return value[:2] + "*" * (len(value) - 4) + value[-2:]
387
+
388
+ def _deep_copy_dict(self, obj: t.Any) -> t.Any:
389
+ """Deep copy a dictionary-like object safely."""
390
+
391
+ if isinstance(obj, dict):
392
+ return {key: self._deep_copy_dict(value) for key, value in obj.items()}
393
+ elif isinstance(obj, list):
394
+ return [self._deep_copy_dict(item) for item in obj]
395
+ return obj
396
+
397
+ def _get_timestamp(self) -> float:
398
+ """Get current timestamp."""
399
+ import time
400
+
401
+ return time.time()
402
+
403
+ def format_error_response(
404
+ self,
405
+ error_message: str,
406
+ verbosity: StatusVerbosity = StatusVerbosity.STANDARD,
407
+ include_details: bool = False,
408
+ ) -> dict[str, t.Any]:
409
+ """
410
+ Format error responses without leaking system details.
411
+
412
+ Args:
413
+ error_message: Original error message
414
+ verbosity: Output verbosity level
415
+ include_details: Whether to include error details
416
+
417
+ Returns:
418
+ Sanitized error response
419
+ """
420
+ error_type = self._classify_error(error_message)
421
+
422
+ if verbosity == StatusVerbosity.MINIMAL:
423
+ return self._create_minimal_error_response(error_type)
424
+
425
+ return self._create_detailed_error_response(
426
+ error_message, error_type, verbosity, include_details
427
+ )
428
+
429
+ def _create_minimal_error_response(self, error_type: str) -> dict[str, t.Any]:
430
+ """Create minimal error response for production use."""
431
+ generic_messages = {
432
+ "connection": "Service temporarily unavailable. Please try again later.",
433
+ "validation": "Invalid request parameters.",
434
+ "permission": "Access denied.",
435
+ "resource": "Requested resource not found.",
436
+ "internal": "An internal error occurred. Please contact support.",
437
+ }
438
+
439
+ return {
440
+ "success": False,
441
+ "error": generic_messages.get(error_type, generic_messages["internal"]),
442
+ "timestamp": self._get_timestamp(),
443
+ }
444
+
445
+ def _create_detailed_error_response(
446
+ self,
447
+ error_message: str,
448
+ error_type: str,
449
+ verbosity: StatusVerbosity,
450
+ include_details: bool,
451
+ ) -> dict[str, t.Any]:
452
+ """Create detailed error response with sanitized message."""
453
+ sanitized_message = self._sanitize_string(error_message, verbosity)
454
+
455
+ response: dict[str, t.Any] = {
456
+ "success": False,
457
+ "error": sanitized_message,
458
+ "error_type": error_type,
459
+ "timestamp": self._get_timestamp(),
460
+ }
461
+
462
+ if self._should_include_error_details(include_details, verbosity):
463
+ response["details"] = {
464
+ "verbosity": str(verbosity.value),
465
+ "sanitized": verbosity != StatusVerbosity.FULL,
466
+ }
467
+
468
+ return response
469
+
470
+ def _should_include_error_details(
471
+ self, include_details: bool, verbosity: StatusVerbosity
472
+ ) -> bool:
473
+ """Determine if error details should be included."""
474
+ return include_details and verbosity in (
475
+ StatusVerbosity.DETAILED,
476
+ StatusVerbosity.FULL,
477
+ )
478
+
479
+ def _classify_error(self, error_message: str) -> str:
480
+ """Classify error message type for appropriate handling."""
481
+
482
+ error_patterns = {
483
+ "connection": ["connection", "timeout", "refused", "unavailable"],
484
+ "validation": ["invalid", "validation", "format", "parameter"],
485
+ "permission": ["permission", "access", "denied", "unauthorized"],
486
+ "resource": ["not found", "missing", "does not exist"],
487
+ }
488
+
489
+ error_lower = error_message.lower()
490
+
491
+ for error_type, patterns in error_patterns.items():
492
+ if any(pattern in error_lower for pattern in patterns):
493
+ return error_type
494
+
495
+ return "internal"
496
+
497
+
498
+ # Singleton instance for global use
499
+ _secure_formatter: SecureStatusFormatter | None = None
500
+
501
+
502
+ def get_secure_status_formatter(
503
+ project_root: Path | None = None,
504
+ ) -> SecureStatusFormatter:
505
+ """Get the global secure status formatter instance."""
506
+
507
+ global _secure_formatter
508
+ if _secure_formatter is None:
509
+ _secure_formatter = SecureStatusFormatter(project_root)
510
+ return _secure_formatter
511
+
512
+
513
+ def format_secure_status(
514
+ status_data: dict[str, t.Any],
515
+ verbosity: StatusVerbosity = StatusVerbosity.STANDARD,
516
+ project_root: Path | None = None,
517
+ user_context: str | None = None,
518
+ ) -> dict[str, t.Any]:
519
+ """
520
+ Convenience function for secure status formatting.
521
+
522
+ Args:
523
+ status_data: Raw status data to sanitize
524
+ verbosity: Output verbosity level
525
+ project_root: Project root for relative path conversion
526
+ user_context: Optional user context for logging
527
+
528
+ Returns:
529
+ Sanitized status data
530
+ """
531
+
532
+ return get_secure_status_formatter(project_root).format_status(
533
+ status_data, verbosity, user_context
534
+ )