empathy-framework 4.6.2__py3-none-any.whl → 4.6.5__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.
Files changed (43) hide show
  1. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/METADATA +53 -11
  2. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/RECORD +43 -35
  3. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/WHEEL +1 -1
  4. empathy_llm_toolkit/agent_factory/crews/health_check.py +7 -4
  5. empathy_llm_toolkit/agent_factory/decorators.py +3 -2
  6. empathy_llm_toolkit/agent_factory/memory_integration.py +6 -2
  7. empathy_llm_toolkit/contextual_patterns.py +5 -2
  8. empathy_llm_toolkit/git_pattern_extractor.py +8 -4
  9. empathy_llm_toolkit/providers.py +4 -3
  10. empathy_os/__init__.py +1 -1
  11. empathy_os/cli/__init__.py +306 -0
  12. empathy_os/cli/__main__.py +26 -0
  13. empathy_os/cli/commands/__init__.py +8 -0
  14. empathy_os/cli/commands/inspection.py +48 -0
  15. empathy_os/cli/commands/memory.py +56 -0
  16. empathy_os/cli/commands/provider.py +86 -0
  17. empathy_os/cli/commands/utilities.py +94 -0
  18. empathy_os/cli/core.py +32 -0
  19. empathy_os/cli.py +379 -38
  20. empathy_os/cli_unified.py +19 -3
  21. empathy_os/config/xml_config.py +8 -3
  22. empathy_os/core.py +37 -4
  23. empathy_os/leverage_points.py +2 -1
  24. empathy_os/memory/short_term.py +57 -3
  25. empathy_os/models/token_estimator.py +16 -9
  26. empathy_os/models/validation.py +7 -1
  27. empathy_os/orchestration/real_tools.py +4 -2
  28. empathy_os/project_index/scanner.py +151 -49
  29. empathy_os/socratic/storage.py +2 -1
  30. empathy_os/socratic/visual_editor.py +9 -4
  31. empathy_os/tier_recommender.py +5 -2
  32. empathy_os/workflow_commands.py +11 -6
  33. empathy_os/workflows/base.py +1 -1
  34. empathy_os/workflows/bug_predict.py +70 -1
  35. empathy_os/workflows/pr_review.py +6 -0
  36. empathy_os/workflows/security_audit.py +13 -0
  37. empathy_os/workflows/test_maintenance.py +3 -2
  38. empathy_os/workflows/tier_tracking.py +50 -2
  39. wizards/discharge_summary_wizard.py +4 -2
  40. wizards/incident_report_wizard.py +4 -2
  41. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/entry_points.txt +0 -0
  42. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/licenses/LICENSE +0 -0
  43. {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/top_level.txt +0 -0
@@ -16,6 +16,7 @@ from datetime import datetime, timedelta
16
16
  from pathlib import Path
17
17
  from typing import Any
18
18
 
19
+ from empathy_os.config import _validate_file_path
19
20
  from empathy_os.logging_config import get_logger
20
21
 
21
22
  logger = get_logger(__name__)
@@ -33,10 +34,11 @@ def _load_patterns(patterns_dir: str = "./patterns") -> dict[str, list]:
33
34
  file_path = patterns_path / f"{pattern_type}.json"
34
35
  if file_path.exists():
35
36
  try:
36
- with open(file_path) as f:
37
+ validated_path = _validate_file_path(str(file_path))
38
+ with open(validated_path) as f:
37
39
  data = json.load(f)
38
40
  patterns[pattern_type] = data.get("patterns", data.get("items", []))
39
- except (OSError, json.JSONDecodeError):
41
+ except (OSError, json.JSONDecodeError, ValueError):
40
42
  pass
41
43
 
42
44
  return patterns
@@ -47,10 +49,11 @@ def _load_stats(empathy_dir: str = ".empathy") -> dict[str, Any]:
47
49
  stats_file = Path(empathy_dir) / "stats.json"
48
50
  if stats_file.exists():
49
51
  try:
50
- with open(stats_file) as f:
52
+ validated_path = _validate_file_path(str(stats_file))
53
+ with open(validated_path) as f:
51
54
  result: dict[str, Any] = json.load(f)
52
55
  return result
53
- except (OSError, json.JSONDecodeError):
56
+ except (OSError, json.JSONDecodeError, ValueError):
54
57
  pass
55
58
  return {"commands": {}, "last_session": None, "patterns_learned": 0}
56
59
 
@@ -60,7 +63,8 @@ def _save_stats(stats: dict, empathy_dir: str = ".empathy") -> None:
60
63
  stats_dir = Path(empathy_dir)
61
64
  stats_dir.mkdir(parents=True, exist_ok=True)
62
65
 
63
- with open(stats_dir / "stats.json", "w") as f:
66
+ validated_path = _validate_file_path(str(stats_dir / "stats.json"))
67
+ with open(validated_path, "w") as f:
64
68
  json.dump(stats, f, indent=2, default=str)
65
69
 
66
70
 
@@ -84,7 +88,8 @@ def _get_tech_debt_trend(patterns_dir: str = "./patterns") -> str:
84
88
  return "unknown"
85
89
 
86
90
  try:
87
- with open(tech_debt_file) as f:
91
+ validated_path = _validate_file_path(str(tech_debt_file))
92
+ with open(validated_path) as f:
88
93
  data = json.load(f)
89
94
 
90
95
  snapshots = data.get("snapshots", [])
@@ -277,7 +277,7 @@ def _save_workflow_run(
277
277
  history.append(run)
278
278
  history = history[-max_history:]
279
279
 
280
- validated_path = _validate_file_path(path)
280
+ validated_path = _validate_file_path(str(path))
281
281
  with open(validated_path, "w") as f:
282
282
  json.dump(history, f, indent=2)
283
283
 
@@ -235,6 +235,8 @@ def _is_dangerous_eval_usage(content: str, file_path: str) -> bool:
235
235
  - Pattern definitions for security scanners
236
236
  - Test fixtures: code written via write_text() or similar for testing
237
237
  - Scanner test files that deliberately contain example bad patterns
238
+ - Docstrings documenting security policies (e.g., "No eval() or exec() usage")
239
+ - Security policy documentation in comments
238
240
 
239
241
  Returns:
240
242
  True if dangerous eval/exec usage is found, False otherwise.
@@ -292,14 +294,22 @@ def _is_dangerous_eval_usage(content: str, file_path: str) -> bool:
292
294
  if "eval(" not in content_without_regex_exec and "exec(" not in content_without_regex_exec:
293
295
  return False
294
296
 
297
+ # Remove docstrings before line-by-line analysis
298
+ # This prevents false positives from documentation that mentions eval/exec
299
+ content_without_docstrings = _remove_docstrings(content)
300
+
295
301
  # Check each line for real dangerous usage
296
- lines = content.splitlines()
302
+ lines = content_without_docstrings.splitlines()
297
303
  for line in lines:
298
304
  # Skip comment lines
299
305
  stripped = line.strip()
300
306
  if stripped.startswith("#") or stripped.startswith("//") or stripped.startswith("*"):
301
307
  continue
302
308
 
309
+ # Skip security policy documentation (e.g., "- No eval() or exec()")
310
+ if _is_security_policy_line(stripped):
311
+ continue
312
+
303
313
  # Check for eval( or exec( in this line
304
314
  if "eval(" not in line and "exec(" not in line:
305
315
  continue
@@ -348,6 +358,65 @@ def _is_dangerous_eval_usage(content: str, file_path: str) -> bool:
348
358
  return False
349
359
 
350
360
 
361
+ def _remove_docstrings(content: str) -> str:
362
+ """Remove docstrings from Python content to avoid false positives.
363
+
364
+ Docstrings often document security policies (e.g., "No eval() usage")
365
+ which should not trigger the scanner.
366
+
367
+ Args:
368
+ content: Python source code
369
+
370
+ Returns:
371
+ Content with docstrings replaced by placeholder comments.
372
+ """
373
+ # Remove triple-quoted strings (docstrings)
374
+ # Match """ ... """ and ''' ... ''' including multiline
375
+ content = re.sub(r'"""[\s\S]*?"""', '# [docstring removed]', content)
376
+ content = re.sub(r"'''[\s\S]*?'''", "# [docstring removed]", content)
377
+ return content
378
+
379
+
380
+ def _is_security_policy_line(line: str) -> bool:
381
+ """Check if a line is documenting security policy rather than using eval/exec.
382
+
383
+ Args:
384
+ line: Stripped line of code
385
+
386
+ Returns:
387
+ True if this appears to be security documentation.
388
+ """
389
+ line_lower = line.lower()
390
+
391
+ # Patterns indicating security policy documentation
392
+ policy_patterns = [
393
+ r"no\s+eval", # "No eval" or "no eval()"
394
+ r"no\s+exec", # "No exec" or "no exec()"
395
+ r"never\s+use\s+eval",
396
+ r"never\s+use\s+exec",
397
+ r"avoid\s+eval",
398
+ r"avoid\s+exec",
399
+ r"don'?t\s+use\s+eval",
400
+ r"don'?t\s+use\s+exec",
401
+ r"prohibited.*eval",
402
+ r"prohibited.*exec",
403
+ r"security.*eval",
404
+ r"security.*exec",
405
+ ]
406
+
407
+ for pattern in policy_patterns:
408
+ if re.search(pattern, line_lower):
409
+ return True
410
+
411
+ # Check for list item documentation (e.g., "- No eval() or exec() usage")
412
+ if line.startswith("-") and ("eval" in line_lower or "exec" in line_lower):
413
+ # If it contains "no", "never", "avoid", it's policy documentation
414
+ if any(word in line_lower for word in ["no ", "never", "avoid", "don't", "prohibited"]):
415
+ return True
416
+
417
+ return False
418
+
419
+
351
420
  # Define step configurations for executor-based execution
352
421
  BUG_PREDICT_STEPS = {
353
422
  "recommend": WorkflowStepConfig(
@@ -126,6 +126,7 @@ class PRReviewWorkflow:
126
126
  diff: str | None = None,
127
127
  files_changed: list[str] | None = None,
128
128
  target_path: str = ".",
129
+ target: str | None = None, # Alias for target_path (compatibility)
129
130
  context: dict | None = None,
130
131
  ) -> PRReviewResult:
131
132
  """Execute comprehensive PR review with both crews.
@@ -134,6 +135,7 @@ class PRReviewWorkflow:
134
135
  diff: PR diff content (auto-generated from git if not provided)
135
136
  files_changed: List of changed files
136
137
  target_path: Path to codebase for security audit
138
+ target: Alias for target_path (for CLI compatibility)
137
139
  context: Additional context
138
140
 
139
141
  Returns:
@@ -144,6 +146,10 @@ class PRReviewWorkflow:
144
146
  files_changed = files_changed or []
145
147
  context = context or {}
146
148
 
149
+ # Support 'target' as alias for 'target_path'
150
+ if target and target_path == ".":
151
+ target_path = target
152
+
147
153
  # Auto-generate diff from git if not provided
148
154
  if not diff:
149
155
  import subprocess
@@ -102,6 +102,19 @@ SECURITY_EXAMPLE_PATHS = [
102
102
  "pii_scrubber.py", # Privacy tool
103
103
  "secure_memdocs", # Secure storage module
104
104
  "/security/", # Security modules
105
+ "/benchmarks/", # Benchmark files with test fixtures
106
+ "benchmark_", # Benchmark files (e.g., benchmark_caching.py)
107
+ "phase_2_setup.py", # Setup file with educational patterns
108
+ ]
109
+
110
+ # Patterns indicating test fixture data (code written to temp files for testing)
111
+ TEST_FIXTURE_PATTERNS = [
112
+ r"SECURITY_TEST_FILES\s*=", # Dict of test fixture code
113
+ r"write_text\s*\(", # Writing test data to temp files
114
+ r"# UNSAFE - DO NOT USE", # Educational comments showing bad patterns
115
+ r"# SAFE -", # Educational comments showing good patterns
116
+ r"# INJECTION RISK", # Educational markers
117
+ r"pragma:\s*allowlist\s*secret", # Explicit allowlist marker
105
118
  ]
106
119
 
107
120
  # Test file patterns - findings here are informational, not critical
@@ -16,6 +16,7 @@ Copyright 2025 Smart AI Memory, LLC
16
16
  Licensed under Fair Source 0.9
17
17
  """
18
18
 
19
+ import heapq
19
20
  import logging
20
21
  from dataclasses import dataclass, field
21
22
  from datetime import datetime
@@ -598,7 +599,7 @@ class TestMaintenanceWorkflow:
598
599
  "lines_of_code": f.lines_of_code,
599
600
  "language": f.language,
600
601
  }
601
- for f in sorted(files, key=lambda x: -x.impact_score)[:limit]
602
+ for f in heapq.nlargest(limit, files, key=lambda x: x.impact_score)
602
603
  ]
603
604
 
604
605
  def get_stale_tests(self, limit: int = 20) -> list[dict[str, Any]]:
@@ -610,7 +611,7 @@ class TestMaintenanceWorkflow:
610
611
  "test_file": f.test_file_path,
611
612
  "staleness_days": f.staleness_days,
612
613
  }
613
- for f in sorted(files, key=lambda x: -x.staleness_days)[:limit]
614
+ for f in heapq.nlargest(limit, files, key=lambda x: x.staleness_days)
614
615
  ]
615
616
 
616
617
  def get_test_health_summary(self) -> dict[str, Any]:
@@ -86,6 +86,11 @@ class WorkflowTierTracker:
86
86
  "premium": 0.450,
87
87
  }
88
88
 
89
+ # Retention policy: keep only this many workflow files
90
+ MAX_WORKFLOW_FILES = 100
91
+ # Only run cleanup every N saves to avoid overhead
92
+ CLEANUP_FREQUENCY = 10
93
+
89
94
  def __init__(
90
95
  self,
91
96
  workflow_name: str,
@@ -302,6 +307,11 @@ class WorkflowTierTracker:
302
307
  # Also update consolidated patterns file
303
308
  self._update_consolidated_patterns(progression)
304
309
 
310
+ # Periodic cleanup of old workflow files (every CLEANUP_FREQUENCY saves)
311
+ workflow_count = len(list(self.patterns_dir.glob("workflow_*.json")))
312
+ if workflow_count > self.MAX_WORKFLOW_FILES + self.CLEANUP_FREQUENCY:
313
+ self._cleanup_old_workflow_files()
314
+
305
315
  return pattern_file
306
316
 
307
317
  except Exception as e:
@@ -439,7 +449,7 @@ class WorkflowTierTracker:
439
449
  return actual_cost * 5 # Conservative multiplier
440
450
 
441
451
  def _update_consolidated_patterns(self, progression: dict[str, Any]):
442
- """Update the consolidated patterns.json file."""
452
+ """Update the consolidated patterns.json file with retention policy."""
443
453
  consolidated_file = self.patterns_dir / "all_patterns.json"
444
454
 
445
455
  try:
@@ -454,13 +464,51 @@ class WorkflowTierTracker:
454
464
  # Add new progression
455
465
  data["patterns"].append(progression)
456
466
 
467
+ # Apply retention policy: keep only MAX_WORKFLOW_FILES patterns
468
+ if len(data["patterns"]) > self.MAX_WORKFLOW_FILES:
469
+ data["patterns"] = data["patterns"][-self.MAX_WORKFLOW_FILES :]
470
+
457
471
  # Save updated file
458
472
  validated_consolidated = _validate_file_path(str(consolidated_file))
459
473
  with open(validated_consolidated, "w") as f:
460
474
  json.dump(data, f, indent=2)
461
475
 
462
- except (OSError, ValueError) as e:
476
+ except (OSError, ValueError, json.JSONDecodeError) as e:
463
477
  logger.warning(f"Could not update consolidated patterns: {e}")
478
+ # If file is corrupted, start fresh
479
+ try:
480
+ data = {"patterns": [progression]}
481
+ validated_consolidated = _validate_file_path(str(consolidated_file))
482
+ with open(validated_consolidated, "w") as f:
483
+ json.dump(data, f, indent=2)
484
+ logger.info("Recreated consolidated patterns file")
485
+ except (OSError, ValueError) as e2:
486
+ logger.warning(f"Could not recreate consolidated patterns: {e2}")
487
+
488
+ def _cleanup_old_workflow_files(self):
489
+ """Remove old workflow files to prevent unbounded growth.
490
+
491
+ Called periodically during save_progression to keep disk usage bounded.
492
+ Keeps only the most recent MAX_WORKFLOW_FILES workflow files.
493
+ """
494
+ try:
495
+ workflow_files = sorted(
496
+ self.patterns_dir.glob("workflow_*.json"),
497
+ key=lambda p: p.stat().st_mtime,
498
+ reverse=True,
499
+ )
500
+
501
+ # Delete files beyond retention limit
502
+ files_to_delete = workflow_files[self.MAX_WORKFLOW_FILES :]
503
+ if files_to_delete:
504
+ for f in files_to_delete:
505
+ try:
506
+ f.unlink()
507
+ except OSError:
508
+ pass # Best effort cleanup
509
+ logger.debug(f"Cleaned up {len(files_to_delete)} old workflow files")
510
+ except OSError as e:
511
+ logger.debug(f"Workflow file cleanup skipped: {e}")
464
512
 
465
513
 
466
514
  def auto_recommend_tier(
@@ -157,7 +157,8 @@ async def _store_wizard_session(wizard_id: str, session_data: dict[str, Any]) ->
157
157
  json.dumps(session_data), # FIXED: use JSON
158
158
  )
159
159
  return True
160
- except Exception:
160
+ except Exception: # noqa: BLE001
161
+ # INTENTIONAL: Graceful degradation - fall back to in-memory storage if Redis fails
161
162
  pass
162
163
  _wizard_sessions[wizard_id] = session_data
163
164
  return True
@@ -174,7 +175,8 @@ async def _get_wizard_session(wizard_id: str) -> dict[str, Any] | None:
174
175
  if session_str:
175
176
  # SECURITY FIX: Use json.loads() instead of ast.literal_eval()
176
177
  return json.loads(session_str)
177
- except Exception:
178
+ except Exception: # noqa: BLE001
179
+ # INTENTIONAL: Graceful degradation - fall back to in-memory storage if Redis fails
178
180
  pass
179
181
  return _wizard_sessions.get(wizard_id)
180
182
 
@@ -143,7 +143,8 @@ async def _store_wizard_session(wizard_id: str, session_data: dict[str, Any]) ->
143
143
  json.dumps(session_data), # FIXED: use JSON
144
144
  )
145
145
  return True
146
- except Exception:
146
+ except Exception: # noqa: BLE001
147
+ # INTENTIONAL: Graceful degradation - fall back to in-memory storage if Redis fails
147
148
  pass
148
149
  _wizard_sessions[wizard_id] = session_data
149
150
  return True
@@ -160,7 +161,8 @@ async def _get_wizard_session(wizard_id: str) -> dict[str, Any] | None:
160
161
  if session_str:
161
162
  # SECURITY FIX: Use json.loads() instead of ast.literal_eval()
162
163
  return json.loads(session_str)
163
- except Exception:
164
+ except Exception: # noqa: BLE001
165
+ # INTENTIONAL: Graceful degradation - fall back to in-memory storage if Redis fails
164
166
  pass
165
167
  return _wizard_sessions.get(wizard_id)
166
168