empathy-framework 5.1.1__py3-none-any.whl → 5.3.0__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-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/METADATA +79 -6
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/RECORD +83 -64
- empathy_os/__init__.py +1 -1
- empathy_os/cache/hybrid.py +5 -1
- empathy_os/cli/commands/batch.py +8 -0
- empathy_os/cli/commands/profiling.py +4 -0
- empathy_os/cli/commands/workflow.py +8 -4
- empathy_os/cli_router.py +9 -0
- empathy_os/config.py +15 -2
- empathy_os/core_modules/__init__.py +15 -0
- empathy_os/dashboard/simple_server.py +62 -30
- empathy_os/mcp/__init__.py +10 -0
- empathy_os/mcp/server.py +506 -0
- empathy_os/memory/control_panel.py +1 -131
- empathy_os/memory/control_panel_support.py +145 -0
- empathy_os/memory/encryption.py +159 -0
- empathy_os/memory/long_term.py +46 -631
- empathy_os/memory/long_term_types.py +99 -0
- empathy_os/memory/mixins/__init__.py +25 -0
- empathy_os/memory/mixins/backend_init_mixin.py +249 -0
- empathy_os/memory/mixins/capabilities_mixin.py +208 -0
- empathy_os/memory/mixins/handoff_mixin.py +208 -0
- empathy_os/memory/mixins/lifecycle_mixin.py +49 -0
- empathy_os/memory/mixins/long_term_mixin.py +352 -0
- empathy_os/memory/mixins/promotion_mixin.py +109 -0
- empathy_os/memory/mixins/short_term_mixin.py +182 -0
- empathy_os/memory/short_term.py +61 -12
- empathy_os/memory/simple_storage.py +302 -0
- empathy_os/memory/storage_backend.py +167 -0
- empathy_os/memory/types.py +8 -3
- empathy_os/memory/unified.py +21 -1120
- empathy_os/meta_workflows/cli_commands/__init__.py +56 -0
- empathy_os/meta_workflows/cli_commands/agent_commands.py +321 -0
- empathy_os/meta_workflows/cli_commands/analytics_commands.py +442 -0
- empathy_os/meta_workflows/cli_commands/config_commands.py +232 -0
- empathy_os/meta_workflows/cli_commands/memory_commands.py +182 -0
- empathy_os/meta_workflows/cli_commands/template_commands.py +354 -0
- empathy_os/meta_workflows/cli_commands/workflow_commands.py +382 -0
- empathy_os/meta_workflows/cli_meta_workflows.py +52 -1802
- empathy_os/models/telemetry/__init__.py +71 -0
- empathy_os/models/telemetry/analytics.py +594 -0
- empathy_os/models/telemetry/backend.py +196 -0
- empathy_os/models/telemetry/data_models.py +431 -0
- empathy_os/models/telemetry/storage.py +489 -0
- empathy_os/orchestration/__init__.py +35 -0
- empathy_os/orchestration/execution_strategies.py +481 -0
- empathy_os/orchestration/meta_orchestrator.py +488 -1
- empathy_os/routing/workflow_registry.py +36 -0
- empathy_os/telemetry/agent_coordination.py +2 -3
- empathy_os/telemetry/agent_tracking.py +26 -7
- empathy_os/telemetry/approval_gates.py +18 -24
- empathy_os/telemetry/cli.py +19 -724
- empathy_os/telemetry/commands/__init__.py +14 -0
- empathy_os/telemetry/commands/dashboard_commands.py +696 -0
- empathy_os/telemetry/event_streaming.py +7 -3
- empathy_os/telemetry/feedback_loop.py +28 -15
- empathy_os/tools.py +183 -0
- empathy_os/workflows/__init__.py +5 -0
- empathy_os/workflows/autonomous_test_gen.py +860 -161
- empathy_os/workflows/base.py +6 -2
- empathy_os/workflows/code_review.py +4 -1
- empathy_os/workflows/document_gen/__init__.py +25 -0
- empathy_os/workflows/document_gen/config.py +30 -0
- empathy_os/workflows/document_gen/report_formatter.py +162 -0
- empathy_os/workflows/{document_gen.py → document_gen/workflow.py} +5 -184
- empathy_os/workflows/output.py +4 -1
- empathy_os/workflows/progress.py +8 -2
- empathy_os/workflows/security_audit.py +2 -2
- empathy_os/workflows/security_audit_phase3.py +7 -4
- empathy_os/workflows/seo_optimization.py +633 -0
- empathy_os/workflows/test_gen/__init__.py +52 -0
- empathy_os/workflows/test_gen/ast_analyzer.py +249 -0
- empathy_os/workflows/test_gen/config.py +88 -0
- empathy_os/workflows/test_gen/data_models.py +38 -0
- empathy_os/workflows/test_gen/report_formatter.py +289 -0
- empathy_os/workflows/test_gen/test_templates.py +381 -0
- empathy_os/workflows/test_gen/workflow.py +655 -0
- empathy_os/workflows/test_gen.py +42 -1905
- empathy_os/cli/parsers/cache 2.py +0 -65
- empathy_os/cli_router 2.py +0 -416
- empathy_os/dashboard/app 2.py +0 -512
- empathy_os/dashboard/simple_server 2.py +0 -403
- empathy_os/dashboard/standalone_server 2.py +0 -536
- empathy_os/memory/types 2.py +0 -441
- empathy_os/models/adaptive_routing 2.py +0 -437
- empathy_os/models/telemetry.py +0 -1660
- empathy_os/project_index/scanner_parallel 2.py +0 -291
- empathy_os/telemetry/agent_coordination 2.py +0 -478
- empathy_os/telemetry/agent_tracking 2.py +0 -350
- empathy_os/telemetry/approval_gates 2.py +0 -563
- empathy_os/telemetry/event_streaming 2.py +0 -405
- empathy_os/telemetry/feedback_loop 2.py +0 -557
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/WHEEL +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/licenses/LICENSE_CHANGE_ANNOUNCEMENT.md +0 -0
- {empathy_framework-5.1.1.dist-info → empathy_framework-5.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Support classes for Memory Control Panel.
|
|
2
|
+
|
|
3
|
+
Rate limiting, authentication, and statistics tracking.
|
|
4
|
+
|
|
5
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
6
|
+
Licensed under Fair Source 0.9
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import structlog
|
|
16
|
+
|
|
17
|
+
logger = structlog.get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RateLimiter:
|
|
21
|
+
"""Simple in-memory rate limiter by IP address."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, window_seconds: int = 60, max_requests: int = 100):
|
|
24
|
+
"""Initialize rate limiter.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
window_seconds: Time window in seconds
|
|
28
|
+
max_requests: Maximum requests allowed per window
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If window_seconds or max_requests is invalid
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
if window_seconds < 1:
|
|
35
|
+
raise ValueError(f"window_seconds must be positive, got {window_seconds}")
|
|
36
|
+
|
|
37
|
+
if max_requests < 1:
|
|
38
|
+
raise ValueError(f"max_requests must be positive, got {max_requests}")
|
|
39
|
+
|
|
40
|
+
self.window_seconds = window_seconds
|
|
41
|
+
self.max_requests = max_requests
|
|
42
|
+
self._requests: dict[str, list[float]] = defaultdict(list)
|
|
43
|
+
|
|
44
|
+
def is_allowed(self, client_ip: str) -> bool:
|
|
45
|
+
"""Check if request is allowed for this IP.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
client_ip: The client IP address
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if allowed, False if rate limited
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
now = time.time()
|
|
55
|
+
window_start = now - self.window_seconds
|
|
56
|
+
|
|
57
|
+
# Clean old entries
|
|
58
|
+
self._requests[client_ip] = [ts for ts in self._requests[client_ip] if ts > window_start]
|
|
59
|
+
|
|
60
|
+
# Check if over limit
|
|
61
|
+
if len(self._requests[client_ip]) >= self.max_requests:
|
|
62
|
+
logger.warning("rate_limit_exceeded", client_ip=client_ip)
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
# Record this request
|
|
66
|
+
self._requests[client_ip].append(now)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
def get_remaining(self, client_ip: str) -> int:
|
|
70
|
+
"""Get remaining requests for this IP."""
|
|
71
|
+
now = time.time()
|
|
72
|
+
window_start = now - self.window_seconds
|
|
73
|
+
recent = [ts for ts in self._requests[client_ip] if ts > window_start]
|
|
74
|
+
return max(0, self.max_requests - len(recent))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class APIKeyAuth:
|
|
78
|
+
"""Simple API key authentication."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, api_key: str | None = None):
|
|
81
|
+
"""Initialize API key auth.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
api_key: The API key to require. If None, reads from
|
|
85
|
+
EMPATHY_MEMORY_API_KEY env var. If still None, auth is disabled.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
self.api_key = api_key or os.environ.get("EMPATHY_MEMORY_API_KEY")
|
|
89
|
+
self.enabled = bool(self.api_key)
|
|
90
|
+
self._key_hash: str | None = None
|
|
91
|
+
if self.enabled and self.api_key:
|
|
92
|
+
# Store hash of API key for comparison
|
|
93
|
+
self._key_hash = hashlib.sha256(self.api_key.encode()).hexdigest()
|
|
94
|
+
logger.info("api_key_auth_enabled")
|
|
95
|
+
else:
|
|
96
|
+
logger.info("api_key_auth_disabled", reason="no_key_configured")
|
|
97
|
+
|
|
98
|
+
def is_valid(self, provided_key: str | None) -> bool:
|
|
99
|
+
"""Check if provided API key is valid.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
provided_key: The key provided in the request
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if valid or auth disabled, False otherwise
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
if not self.enabled:
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
if not provided_key:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Constant-time comparison via hash
|
|
115
|
+
provided_hash = hashlib.sha256(provided_key.encode()).hexdigest()
|
|
116
|
+
return provided_hash == self._key_hash
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class MemoryStats:
|
|
121
|
+
"""Statistics for memory system."""
|
|
122
|
+
|
|
123
|
+
# Redis stats
|
|
124
|
+
redis_available: bool = False
|
|
125
|
+
redis_method: str = "none"
|
|
126
|
+
redis_keys_total: int = 0
|
|
127
|
+
redis_keys_working: int = 0
|
|
128
|
+
redis_keys_staged: int = 0
|
|
129
|
+
redis_memory_used: str = "0"
|
|
130
|
+
|
|
131
|
+
# Long-term stats
|
|
132
|
+
long_term_available: bool = False
|
|
133
|
+
patterns_total: int = 0
|
|
134
|
+
patterns_public: int = 0
|
|
135
|
+
patterns_internal: int = 0
|
|
136
|
+
patterns_sensitive: int = 0
|
|
137
|
+
patterns_encrypted: int = 0
|
|
138
|
+
|
|
139
|
+
# Performance stats
|
|
140
|
+
redis_ping_ms: float = 0.0
|
|
141
|
+
storage_bytes: int = 0
|
|
142
|
+
collection_time_ms: float = 0.0
|
|
143
|
+
|
|
144
|
+
# Timestamps
|
|
145
|
+
collected_at: str = ""
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Encryption manager for long-term memory system
|
|
2
|
+
|
|
3
|
+
Provides AES-256-GCM encryption/decryption for SENSITIVE patterns.
|
|
4
|
+
Extracted from long_term.py for better modularity and testability.
|
|
5
|
+
|
|
6
|
+
Key Features:
|
|
7
|
+
- AES-256-GCM authenticated encryption
|
|
8
|
+
- Master key management (environment variable, file, or generated)
|
|
9
|
+
- Base64-encoded output for safe storage
|
|
10
|
+
- Proper error handling and security logging
|
|
11
|
+
|
|
12
|
+
Copyright 2025 Smart AI Memory, LLC
|
|
13
|
+
Licensed under Fair Source 0.9
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
import binascii
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import structlog
|
|
22
|
+
|
|
23
|
+
from .long_term_types import SecurityError
|
|
24
|
+
|
|
25
|
+
logger = structlog.get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
# Check for cryptography library
|
|
28
|
+
try:
|
|
29
|
+
from cryptography.exceptions import InvalidTag
|
|
30
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
31
|
+
|
|
32
|
+
HAS_ENCRYPTION = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
HAS_ENCRYPTION = False
|
|
35
|
+
logger.warning("cryptography library not available - encryption disabled")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EncryptionManager:
|
|
39
|
+
"""Manages encryption/decryption for SENSITIVE patterns.
|
|
40
|
+
|
|
41
|
+
Uses AES-256-GCM (Galois/Counter Mode) for authenticated encryption.
|
|
42
|
+
Keys are derived from a master key using HKDF.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, master_key: bytes | None = None):
|
|
46
|
+
"""Initialize encryption manager.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
master_key: 32-byte master key (or None to generate/load)
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
if not HAS_ENCRYPTION:
|
|
53
|
+
logger.warning("Encryption not available - install cryptography library")
|
|
54
|
+
self.enabled = False
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
self.enabled = True
|
|
58
|
+
self.master_key = master_key or self._load_or_generate_key()
|
|
59
|
+
|
|
60
|
+
def _load_or_generate_key(self) -> bytes:
|
|
61
|
+
"""Load master key from environment or generate new one.
|
|
62
|
+
|
|
63
|
+
Production: Set EMPATHY_MASTER_KEY environment variable
|
|
64
|
+
Development: Generates ephemeral key (warning logged)
|
|
65
|
+
"""
|
|
66
|
+
# Check environment variable first
|
|
67
|
+
if env_key := os.getenv("EMPATHY_MASTER_KEY"):
|
|
68
|
+
try:
|
|
69
|
+
return base64.b64decode(env_key)
|
|
70
|
+
except (binascii.Error, ValueError) as e:
|
|
71
|
+
logger.error("invalid_master_key_in_env", error=str(e))
|
|
72
|
+
raise ValueError("Invalid EMPATHY_MASTER_KEY format") from e
|
|
73
|
+
|
|
74
|
+
# Check key file
|
|
75
|
+
key_file = Path.home() / ".empathy" / "master.key"
|
|
76
|
+
if key_file.exists():
|
|
77
|
+
try:
|
|
78
|
+
return key_file.read_bytes()
|
|
79
|
+
except (OSError, PermissionError) as e:
|
|
80
|
+
logger.error("failed_to_load_key_file", error=str(e))
|
|
81
|
+
|
|
82
|
+
# Generate ephemeral key (NOT for production)
|
|
83
|
+
logger.warning(
|
|
84
|
+
"no_master_key_found",
|
|
85
|
+
message="Generating ephemeral encryption key - set EMPATHY_MASTER_KEY for production",
|
|
86
|
+
)
|
|
87
|
+
return AESGCM.generate_key(bit_length=256)
|
|
88
|
+
|
|
89
|
+
def encrypt(self, plaintext: str) -> str:
|
|
90
|
+
"""Encrypt plaintext using AES-256-GCM.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
plaintext: Content to encrypt
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Base64-encoded ciphertext with format: nonce||ciphertext||tag
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
SecurityError: If encryption fails
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
if not self.enabled:
|
|
103
|
+
raise SecurityError("Encryption not available - install cryptography library")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# Generate random 96-bit nonce (12 bytes)
|
|
107
|
+
nonce = os.urandom(12)
|
|
108
|
+
|
|
109
|
+
# Create AESGCM cipher
|
|
110
|
+
aesgcm = AESGCM(self.master_key)
|
|
111
|
+
|
|
112
|
+
# Encrypt and authenticate
|
|
113
|
+
ciphertext = aesgcm.encrypt(nonce, plaintext.encode("utf-8"), None)
|
|
114
|
+
|
|
115
|
+
# Combine nonce + ciphertext for storage
|
|
116
|
+
encrypted_data = nonce + ciphertext
|
|
117
|
+
|
|
118
|
+
# Return base64-encoded
|
|
119
|
+
return base64.b64encode(encrypted_data).decode("utf-8")
|
|
120
|
+
|
|
121
|
+
except (ValueError, TypeError, UnicodeEncodeError) as e:
|
|
122
|
+
logger.error("encryption_failed", error=str(e))
|
|
123
|
+
raise SecurityError(f"Encryption failed: {e}") from e
|
|
124
|
+
|
|
125
|
+
def decrypt(self, ciphertext_b64: str) -> str:
|
|
126
|
+
"""Decrypt ciphertext using AES-256-GCM.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
ciphertext_b64: Base64-encoded encrypted data
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Decrypted plaintext
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
SecurityError: If decryption fails (invalid key, corrupted data, etc.)
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
if not self.enabled:
|
|
139
|
+
raise SecurityError("Encryption not available - install cryptography library")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
# Decode from base64
|
|
143
|
+
encrypted_data = base64.b64decode(ciphertext_b64)
|
|
144
|
+
|
|
145
|
+
# Extract nonce (first 12 bytes) and ciphertext (rest)
|
|
146
|
+
nonce = encrypted_data[:12]
|
|
147
|
+
ciphertext = encrypted_data[12:]
|
|
148
|
+
|
|
149
|
+
# Create AESGCM cipher
|
|
150
|
+
aesgcm = AESGCM(self.master_key)
|
|
151
|
+
|
|
152
|
+
# Decrypt and verify
|
|
153
|
+
plaintext_bytes = aesgcm.decrypt(nonce, ciphertext, None)
|
|
154
|
+
|
|
155
|
+
return plaintext_bytes.decode("utf-8")
|
|
156
|
+
|
|
157
|
+
except (ValueError, TypeError, UnicodeDecodeError, binascii.Error, InvalidTag) as e:
|
|
158
|
+
logger.error("decryption_failed", error=str(e))
|
|
159
|
+
raise SecurityError(f"Decryption failed: {e}") from e
|