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.
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/METADATA +53 -11
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/RECORD +43 -35
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/WHEEL +1 -1
- empathy_llm_toolkit/agent_factory/crews/health_check.py +7 -4
- empathy_llm_toolkit/agent_factory/decorators.py +3 -2
- empathy_llm_toolkit/agent_factory/memory_integration.py +6 -2
- empathy_llm_toolkit/contextual_patterns.py +5 -2
- empathy_llm_toolkit/git_pattern_extractor.py +8 -4
- empathy_llm_toolkit/providers.py +4 -3
- empathy_os/__init__.py +1 -1
- empathy_os/cli/__init__.py +306 -0
- empathy_os/cli/__main__.py +26 -0
- empathy_os/cli/commands/__init__.py +8 -0
- empathy_os/cli/commands/inspection.py +48 -0
- empathy_os/cli/commands/memory.py +56 -0
- empathy_os/cli/commands/provider.py +86 -0
- empathy_os/cli/commands/utilities.py +94 -0
- empathy_os/cli/core.py +32 -0
- empathy_os/cli.py +379 -38
- empathy_os/cli_unified.py +19 -3
- empathy_os/config/xml_config.py +8 -3
- empathy_os/core.py +37 -4
- empathy_os/leverage_points.py +2 -1
- empathy_os/memory/short_term.py +57 -3
- empathy_os/models/token_estimator.py +16 -9
- empathy_os/models/validation.py +7 -1
- empathy_os/orchestration/real_tools.py +4 -2
- empathy_os/project_index/scanner.py +151 -49
- empathy_os/socratic/storage.py +2 -1
- empathy_os/socratic/visual_editor.py +9 -4
- empathy_os/tier_recommender.py +5 -2
- empathy_os/workflow_commands.py +11 -6
- empathy_os/workflows/base.py +1 -1
- empathy_os/workflows/bug_predict.py +70 -1
- empathy_os/workflows/pr_review.py +6 -0
- empathy_os/workflows/security_audit.py +13 -0
- empathy_os/workflows/test_maintenance.py +3 -2
- empathy_os/workflows/tier_tracking.py +50 -2
- wizards/discharge_summary_wizard.py +4 -2
- wizards/incident_report_wizard.py +4 -2
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/entry_points.txt +0 -0
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-4.6.2.dist-info → empathy_framework-4.6.5.dist-info}/top_level.txt +0 -0
empathy_os/workflow_commands.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", [])
|
empathy_os/workflows/base.py
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|