feat(02-04): Create safety coordinator
Some checks failed
Discord Webhook / git (push) Has been cancelled
Some checks failed
Discord Webhook / git (push) Has been cancelled
This commit is contained in:
5
src/safety/__init__.py
Normal file
5
src/safety/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Safety and sandboxing coordination module."""
|
||||
|
||||
from .coordinator import SafetyCoordinator
|
||||
|
||||
__all__ = ["SafetyCoordinator"]
|
||||
386
src/safety/coordinator.py
Normal file
386
src/safety/coordinator.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""Safety coordinator for orchestrating security assessment, sandbox execution, and audit logging."""
|
||||
|
||||
import logging
|
||||
import psutil
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
from ..security.assessor import SecurityAssessor, SecurityLevel
|
||||
from ..sandbox.executor import SandboxExecutor
|
||||
from ..audit.logger import AuditLogger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SafetyCoordinator:
|
||||
"""
|
||||
Main safety coordination logic that orchestrates all safety components.
|
||||
|
||||
Coordinates security assessment, sandbox execution, and audit logging
|
||||
with user override capability and adaptive resource management.
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: Optional[str] = None):
|
||||
"""
|
||||
Initialize safety coordinator with component configurations.
|
||||
|
||||
Args:
|
||||
config_path: Optional path to safety configuration directory
|
||||
"""
|
||||
self.config_path = config_path or "config/safety"
|
||||
|
||||
# Initialize components
|
||||
self.security_assessor = SecurityAssessor(
|
||||
config_path=str(Path(self.config_path) / "security.yaml")
|
||||
)
|
||||
self.sandbox_executor = SandboxExecutor(
|
||||
config_path=str(Path(self.config_path) / "sandbox.yaml")
|
||||
)
|
||||
self.audit_logger = AuditLogger(
|
||||
log_file=str(Path(self.config_path) / "audit.log"),
|
||||
storage_dir=str(Path(self.config_path) / "audit"),
|
||||
)
|
||||
|
||||
# System resource monitoring
|
||||
self.system_monitor = psutil
|
||||
|
||||
def execute_code_safely(
|
||||
self,
|
||||
code: str,
|
||||
user_override: bool = False,
|
||||
user_explanation: Optional[str] = None,
|
||||
metadata: Optional[Dict] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute code with full safety coordination.
|
||||
|
||||
Args:
|
||||
code: Python code to execute
|
||||
user_override: Whether user is overriding security decision
|
||||
user_explanation: Explanation for user override
|
||||
metadata: Additional execution metadata
|
||||
|
||||
Returns:
|
||||
Execution result with comprehensive safety metadata
|
||||
"""
|
||||
execution_id = self._generate_execution_id()
|
||||
metadata = metadata or {}
|
||||
metadata["execution_id"] = execution_id
|
||||
|
||||
logger.info(f"Starting safe execution: {execution_id}")
|
||||
|
||||
try:
|
||||
# Step 1: Security assessment
|
||||
security_level, security_findings = self.security_assessor.assess(code)
|
||||
|
||||
# Log security assessment
|
||||
self.audit_logger.log_security_assessment(
|
||||
assessment={
|
||||
"security_level": security_level.value,
|
||||
"security_score": security_findings.get("security_score", 0),
|
||||
"findings": security_findings,
|
||||
"recommendations": security_findings.get("recommendations", []),
|
||||
},
|
||||
code_snippet=code,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
# Step 2: Check if execution should proceed
|
||||
should_execute, override_used = self._check_execution_permission(
|
||||
security_level, user_override, user_explanation, metadata
|
||||
)
|
||||
|
||||
if not should_execute:
|
||||
result = {
|
||||
"success": False,
|
||||
"blocked": True,
|
||||
"security_level": security_level.value,
|
||||
"security_findings": security_findings,
|
||||
"reason": "Code blocked by security assessment",
|
||||
"execution_id": execution_id,
|
||||
"override_used": False,
|
||||
}
|
||||
|
||||
# Log blocked execution
|
||||
self.audit_logger.log_security_event(
|
||||
event_type="execution_blocked",
|
||||
details={
|
||||
"security_level": security_level.value,
|
||||
"security_score": security_findings.get("security_score", 0),
|
||||
"reason": result["reason"],
|
||||
},
|
||||
severity="HIGH",
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
# Step 3: Determine adaptive resource limits
|
||||
trust_level = self._determine_trust_level(security_level, override_used)
|
||||
resource_limits = self._get_adaptive_resource_limits(code, trust_level)
|
||||
|
||||
# Step 4: Execute in sandbox
|
||||
execution_result = self.sandbox_executor.execute_code(
|
||||
code, trust_level=trust_level
|
||||
)
|
||||
|
||||
# Step 5: Log execution
|
||||
self.audit_logger.log_code_execution(
|
||||
code=code,
|
||||
result=execution_result,
|
||||
execution_time=execution_result.get("execution_time"),
|
||||
security_level=security_level.value,
|
||||
metadata={
|
||||
**metadata,
|
||||
"resource_limits": resource_limits,
|
||||
"trust_level": trust_level,
|
||||
"override_used": override_used,
|
||||
},
|
||||
)
|
||||
|
||||
# Step 6: Return comprehensive result
|
||||
return {
|
||||
"success": execution_result["success"],
|
||||
"execution_id": execution_id,
|
||||
"security_level": security_level.value,
|
||||
"security_findings": security_findings,
|
||||
"execution_result": execution_result,
|
||||
"resource_limits": resource_limits,
|
||||
"trust_level": trust_level,
|
||||
"override_used": override_used,
|
||||
"blocked": False,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Safety coordination failed: {e}")
|
||||
|
||||
# Log error
|
||||
self.audit_logger.log_security_event(
|
||||
event_type="safety_coordination_error",
|
||||
details={"error": str(e), "execution_id": execution_id},
|
||||
severity="CRITICAL",
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"execution_id": execution_id,
|
||||
"error": str(e),
|
||||
"blocked": False,
|
||||
"override_used": False,
|
||||
}
|
||||
|
||||
def _check_execution_permission(
|
||||
self,
|
||||
security_level: SecurityLevel,
|
||||
user_override: bool,
|
||||
user_explanation: Optional[str],
|
||||
metadata: Dict,
|
||||
) -> Tuple[bool, bool]:
|
||||
"""
|
||||
Check if execution should proceed based on security level and user override.
|
||||
|
||||
Args:
|
||||
security_level: Assessed security level
|
||||
user_override: Whether user requested override
|
||||
user_explanation: User's explanation for override
|
||||
metadata: Execution metadata
|
||||
|
||||
Returns:
|
||||
Tuple of (should_execute, override_used)
|
||||
"""
|
||||
override_used = False
|
||||
|
||||
# LOW and MEDIUM: Always allow
|
||||
if security_level in [SecurityLevel.LOW, SecurityLevel.MEDIUM]:
|
||||
return True, override_used
|
||||
|
||||
# HIGH: Allow with warning
|
||||
if security_level == SecurityLevel.HIGH:
|
||||
logger.warning(
|
||||
f"High risk code execution approved: {metadata.get('execution_id')}"
|
||||
)
|
||||
return True, override_used
|
||||
|
||||
# BLOCKED: Only allow with explicit user override
|
||||
if security_level == SecurityLevel.BLOCKED:
|
||||
if user_override and user_explanation:
|
||||
override_used = True
|
||||
logger.warning(
|
||||
f"User override for BLOCKED code: {user_explanation} "
|
||||
f"(execution_id: {metadata.get('execution_id')})"
|
||||
)
|
||||
|
||||
# Log override event
|
||||
self.audit_logger.log_security_event(
|
||||
event_type="user_override",
|
||||
details={
|
||||
"original_security_level": security_level.value,
|
||||
"user_explanation": user_explanation,
|
||||
},
|
||||
severity="HIGH",
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
return True, override_used
|
||||
else:
|
||||
return False, override_used
|
||||
|
||||
return False, override_used
|
||||
|
||||
def _determine_trust_level(
|
||||
self, security_level: SecurityLevel, override_used: bool
|
||||
) -> str:
|
||||
"""
|
||||
Determine trust level for sandbox execution based on security assessment.
|
||||
|
||||
Args:
|
||||
security_level: Assessed security level
|
||||
override_used: Whether user override was used
|
||||
|
||||
Returns:
|
||||
Trust level string for sandbox configuration
|
||||
"""
|
||||
if override_used:
|
||||
return "untrusted" # Use most restrictive for overrides
|
||||
|
||||
if security_level == SecurityLevel.LOW:
|
||||
return "trusted"
|
||||
elif security_level == SecurityLevel.MEDIUM:
|
||||
return "standard"
|
||||
else: # HIGH or BLOCKED (with override)
|
||||
return "untrusted"
|
||||
|
||||
def _get_adaptive_resource_limits(
|
||||
self, code: str, trust_level: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate adaptive resource limits based on code complexity and system resources.
|
||||
|
||||
Args:
|
||||
code: Code to analyze for complexity
|
||||
trust_level: Trust level affecting base limits
|
||||
|
||||
Returns:
|
||||
Resource limit configuration
|
||||
"""
|
||||
# Get system resources
|
||||
cpu_count = self.system_monitor.cpu_count()
|
||||
memory_info = self.system_monitor.virtual_memory()
|
||||
available_memory_gb = memory_info.available / (1024**3)
|
||||
|
||||
# Analyze code complexity
|
||||
code_lines = len(code.splitlines())
|
||||
code_complexity = self._analyze_code_complexity(code)
|
||||
|
||||
# Base limits by trust level
|
||||
if trust_level == "trusted":
|
||||
base_cpu = min(cpu_count - 1, 4)
|
||||
base_memory = min(available_memory_gb * 0.3, 4.0)
|
||||
base_timeout = 300
|
||||
elif trust_level == "standard":
|
||||
base_cpu = min(cpu_count // 2, 2)
|
||||
base_memory = min(available_memory_gb * 0.15, 2.0)
|
||||
base_timeout = 120
|
||||
else: # untrusted
|
||||
base_cpu = 1
|
||||
base_memory = min(available_memory_gb * 0.05, 0.5)
|
||||
base_timeout = 30
|
||||
|
||||
# Adjust for code complexity
|
||||
complexity_multiplier = 1.0 + (code_complexity * 0.2)
|
||||
complexity_multiplier = min(complexity_multiplier, 2.0) # Cap at 2x
|
||||
|
||||
# Final limits
|
||||
resource_limits = {
|
||||
"cpu_count": max(1, int(base_cpu * complexity_multiplier)),
|
||||
"memory_limit_gb": round(base_memory * complexity_multiplier, 2),
|
||||
"timeout_seconds": int(base_timeout * complexity_multiplier),
|
||||
"code_lines": code_lines,
|
||||
"complexity_score": code_complexity,
|
||||
"system_cpu_count": cpu_count,
|
||||
"system_memory_gb": round(available_memory_gb, 2),
|
||||
}
|
||||
|
||||
return resource_limits
|
||||
|
||||
def _analyze_code_complexity(self, code: str) -> float:
|
||||
"""
|
||||
Analyze code complexity for resource allocation.
|
||||
|
||||
Args:
|
||||
code: Code to analyze
|
||||
|
||||
Returns:
|
||||
Complexity score (0.0 to 1.0)
|
||||
"""
|
||||
complexity_score = 0.0
|
||||
|
||||
# Line count factor
|
||||
lines = len(code.splitlines())
|
||||
complexity_score += min(lines / 100, 0.3) # Max 0.3 for line count
|
||||
|
||||
# Control flow structures
|
||||
control_keywords = ["if", "for", "while", "try", "with", "def", "class"]
|
||||
for keyword in control_keywords:
|
||||
count = code.count(keyword)
|
||||
complexity_score += min(count / 20, 0.1) # Max 0.1 per keyword type
|
||||
|
||||
# Import statements
|
||||
import_count = code.count("import") + code.count("from")
|
||||
complexity_score += min(import_count / 10, 0.1)
|
||||
|
||||
# String operations (potential for heavy processing)
|
||||
string_ops = code.count(".format") + code.count("f'") + code.count('f"')
|
||||
complexity_score += min(string_ops / 10, 0.1)
|
||||
|
||||
return min(complexity_score, 1.0)
|
||||
|
||||
def _generate_execution_id(self) -> str:
|
||||
"""Generate unique execution ID."""
|
||||
import uuid
|
||||
|
||||
return str(uuid.uuid4())[:8]
|
||||
|
||||
def get_security_status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get current security system status.
|
||||
|
||||
Returns:
|
||||
Security system status information
|
||||
"""
|
||||
return {
|
||||
"security_assessor": "active",
|
||||
"sandbox_executor": "active",
|
||||
"audit_logger": "active",
|
||||
"system_resources": {
|
||||
"cpu_count": self.system_monitor.cpu_count(),
|
||||
"memory_total_gb": round(
|
||||
self.system_monitor.virtual_memory().total / (1024**3), 2
|
||||
),
|
||||
"memory_available_gb": round(
|
||||
self.system_monitor.virtual_memory().available / (1024**3), 2
|
||||
),
|
||||
},
|
||||
"audit_integrity": self.audit_logger.verify_integrity(),
|
||||
}
|
||||
|
||||
def get_execution_history(self, limit: int = 10) -> Dict[str, Any]:
|
||||
"""
|
||||
Get recent execution history from audit logs.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of entries to return
|
||||
|
||||
Returns:
|
||||
Recent execution history
|
||||
"""
|
||||
# Get security summary for recent executions
|
||||
summary = self.audit_logger.get_security_summary(time_range_hours=24)
|
||||
|
||||
return {
|
||||
"summary": summary,
|
||||
"recent_executions": summary.get("code_executions", 0),
|
||||
"security_assessments": summary.get("security_assessments", 0),
|
||||
"resource_violations": summary.get("resource_violations", 0),
|
||||
}
|
||||
Reference in New Issue
Block a user