feat(02-04): Implement safety API interface
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:
@@ -1,5 +1,6 @@
|
|||||||
"""Safety and sandboxing coordination module."""
|
"""Safety and sandboxing coordination module."""
|
||||||
|
|
||||||
from .coordinator import SafetyCoordinator
|
from .coordinator import SafetyCoordinator
|
||||||
|
from .api import SafetyAPI
|
||||||
|
|
||||||
__all__ = ["SafetyCoordinator"]
|
__all__ = ["SafetyCoordinator", "SafetyAPI"]
|
||||||
|
|||||||
335
src/safety/api.py
Normal file
335
src/safety/api.py
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
"""Public API interface for safety system."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .coordinator import SafetyCoordinator
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SafetyAPI:
|
||||||
|
"""
|
||||||
|
Public interface for safety functionality.
|
||||||
|
|
||||||
|
Provides clean, validated interface for other system components
|
||||||
|
to use safety functionality including code assessment and execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config_path: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Initialize safety API with coordinator backend.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Optional path to safety configuration
|
||||||
|
"""
|
||||||
|
self.coordinator = SafetyCoordinator(config_path)
|
||||||
|
|
||||||
|
def assess_and_execute(
|
||||||
|
self,
|
||||||
|
code: str,
|
||||||
|
user_override: bool = False,
|
||||||
|
user_explanation: Optional[str] = None,
|
||||||
|
metadata: Optional[Dict] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Assess and execute code with full safety coordination.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python code to assess and execute
|
||||||
|
user_override: Whether user wants to override security decision
|
||||||
|
user_explanation: Required explanation for override
|
||||||
|
metadata: Additional execution metadata
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted execution result with security metadata
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If input validation fails
|
||||||
|
"""
|
||||||
|
# Input validation
|
||||||
|
validation_result = self._validate_code_input(
|
||||||
|
code, user_override, user_explanation
|
||||||
|
)
|
||||||
|
if not validation_result["valid"]:
|
||||||
|
raise ValueError(validation_result["error"])
|
||||||
|
|
||||||
|
# Execute through coordinator
|
||||||
|
result = self.coordinator.execute_code_safely(
|
||||||
|
code=code,
|
||||||
|
user_override=user_override,
|
||||||
|
user_explanation=user_explanation,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format response
|
||||||
|
return self._format_execution_response(result)
|
||||||
|
|
||||||
|
def assess_code_only(self, code: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Assess code security without execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Python code to assess
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Security assessment results
|
||||||
|
"""
|
||||||
|
if not code or not code.strip():
|
||||||
|
raise ValueError("Code cannot be empty")
|
||||||
|
|
||||||
|
security_level, findings = self.coordinator.security_assessor.assess(code)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"security_level": security_level.value,
|
||||||
|
"security_score": findings.get("security_score", 0),
|
||||||
|
"findings": findings,
|
||||||
|
"recommendations": findings.get("recommendations", []),
|
||||||
|
"assessed_at": datetime.utcnow().isoformat(),
|
||||||
|
"can_execute": security_level.value != "BLOCKED",
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_execution_history(self, limit: int = 10) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get recent execution history.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
limit: Maximum number of entries to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted execution history
|
||||||
|
"""
|
||||||
|
if not isinstance(limit, int) or limit <= 0:
|
||||||
|
raise ValueError("Limit must be a positive integer")
|
||||||
|
|
||||||
|
history = self.coordinator.get_execution_history(limit)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"request": {"limit": limit},
|
||||||
|
"response": history,
|
||||||
|
"retrieved_at": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_security_status(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get current security system status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Security system status and health information
|
||||||
|
"""
|
||||||
|
status = self.coordinator.get_security_status()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"system_status": "operational"
|
||||||
|
if all(
|
||||||
|
component == "active"
|
||||||
|
for component in [
|
||||||
|
status.get("security_assessor"),
|
||||||
|
status.get("sandbox_executor"),
|
||||||
|
status.get("audit_logger"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else "degraded",
|
||||||
|
"components": {
|
||||||
|
"security_assessor": status.get("security_assessor"),
|
||||||
|
"sandbox_executor": status.get("sandbox_executor"),
|
||||||
|
"audit_logger": status.get("audit_logger"),
|
||||||
|
},
|
||||||
|
"system_resources": status.get("system_resources", {}),
|
||||||
|
"audit_integrity": status.get("audit_integrity", {}),
|
||||||
|
"status_checked_at": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def configure_policies(self, policies: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Update security and sandbox policies.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
policies: Policy configuration dictionary
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Configuration update results
|
||||||
|
"""
|
||||||
|
if not isinstance(policies, dict):
|
||||||
|
raise ValueError("Policies must be a dictionary")
|
||||||
|
|
||||||
|
update_results = {
|
||||||
|
"updated_policies": [],
|
||||||
|
"failed_updates": [],
|
||||||
|
"validation_errors": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate and update security policies
|
||||||
|
if "security" in policies:
|
||||||
|
try:
|
||||||
|
self._validate_security_policies(policies["security"])
|
||||||
|
# Note: In a real implementation, this would update the assessor config
|
||||||
|
update_results["updated_policies"].append("security")
|
||||||
|
except Exception as e:
|
||||||
|
update_results["failed_updates"].append("security")
|
||||||
|
update_results["validation_errors"].append(
|
||||||
|
f"Security policies: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate and update sandbox policies
|
||||||
|
if "sandbox" in policies:
|
||||||
|
try:
|
||||||
|
self._validate_sandbox_policies(policies["sandbox"])
|
||||||
|
# Note: In a real implementation, this would update the executor config
|
||||||
|
update_results["updated_policies"].append("sandbox")
|
||||||
|
except Exception as e:
|
||||||
|
update_results["failed_updates"].append("sandbox")
|
||||||
|
update_results["validation_errors"].append(
|
||||||
|
f"Sandbox policies: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"request": {"policies": list(policies.keys())},
|
||||||
|
"response": update_results,
|
||||||
|
"updated_at": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_audit_report(
|
||||||
|
self, time_range_hours: Optional[int] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get comprehensive audit report.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_range_hours: Optional time filter for report
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Audit report data
|
||||||
|
"""
|
||||||
|
if time_range_hours is not None:
|
||||||
|
if not isinstance(time_range_hours, int) or time_range_hours <= 0:
|
||||||
|
raise ValueError("time_range_hours must be a positive integer")
|
||||||
|
|
||||||
|
# Get security summary
|
||||||
|
summary = self.coordinator.audit_logger.get_security_summary(
|
||||||
|
time_range_hours or 24
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get integrity check
|
||||||
|
integrity = self.coordinator.audit_logger.verify_integrity()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"report_period_hours": time_range_hours or 24,
|
||||||
|
"summary": summary,
|
||||||
|
"integrity_check": integrity,
|
||||||
|
"report_generated_at": datetime.utcnow().isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _validate_code_input(
|
||||||
|
self, code: str, user_override: bool, user_explanation: Optional[str]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Validate code execution input parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Code to validate
|
||||||
|
user_override: Override flag
|
||||||
|
user_explanation: Override explanation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Validation result with error if invalid
|
||||||
|
"""
|
||||||
|
if not code or not code.strip():
|
||||||
|
return {"valid": False, "error": "Code cannot be empty"}
|
||||||
|
|
||||||
|
if len(code) > 100000: # 100KB limit
|
||||||
|
return {"valid": False, "error": "Code too large (max 100KB)"}
|
||||||
|
|
||||||
|
if user_override and not user_explanation:
|
||||||
|
return {"valid": False, "error": "User override requires explanation"}
|
||||||
|
|
||||||
|
if user_explanation and len(user_explanation) > 500:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"error": "Override explanation too long (max 500 characters)",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"valid": True}
|
||||||
|
|
||||||
|
def _format_execution_response(self, result: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Format execution result for API response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: Raw execution result from coordinator
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted API response
|
||||||
|
"""
|
||||||
|
response = {
|
||||||
|
"request_id": result.get("execution_id"),
|
||||||
|
"success": result.get("success", False),
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"security": {
|
||||||
|
"level": result.get("security_level"),
|
||||||
|
"override_used": result.get("override_used", False),
|
||||||
|
"findings": result.get("security_findings", {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.get("blocked"):
|
||||||
|
response["blocked"] = True
|
||||||
|
response["reason"] = result.get(
|
||||||
|
"reason", "Security assessment blocked execution"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response["execution"] = result.get("execution_result", {})
|
||||||
|
response["resource_limits"] = result.get("resource_limits", {})
|
||||||
|
response["trust_level"] = result.get("trust_level")
|
||||||
|
|
||||||
|
if "error" in result:
|
||||||
|
response["error"] = result["error"]
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _validate_security_policies(self, policies: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Validate security policy configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
policies: Security policies to validate
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If policies are invalid
|
||||||
|
"""
|
||||||
|
required_keys = ["blocked_patterns", "high_triggers", "thresholds"]
|
||||||
|
for key in required_keys:
|
||||||
|
if key not in policies:
|
||||||
|
raise ValueError(f"Missing required security policy: {key}")
|
||||||
|
|
||||||
|
# Validate thresholds
|
||||||
|
thresholds = policies["thresholds"]
|
||||||
|
if not all(isinstance(v, (int, float)) and v >= 0 for v in thresholds.values()):
|
||||||
|
raise ValueError("Security thresholds must be non-negative numbers")
|
||||||
|
|
||||||
|
def _validate_sandbox_policies(self, policies: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Validate sandbox policy configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
policies: Sandbox policies to validate
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If policies are invalid
|
||||||
|
"""
|
||||||
|
if "resources" in policies:
|
||||||
|
resources = policies["resources"]
|
||||||
|
|
||||||
|
# Validate timeout
|
||||||
|
if "timeout" in resources and not (
|
||||||
|
isinstance(resources["timeout"], (int, float))
|
||||||
|
and resources["timeout"] > 0
|
||||||
|
):
|
||||||
|
raise ValueError("Timeout must be a positive number")
|
||||||
|
|
||||||
|
# Validate memory limit
|
||||||
|
if "mem_limit" in resources:
|
||||||
|
mem_limit = str(resources["mem_limit"])
|
||||||
|
if not (mem_limit.endswith(("g", "m", "k")) or mem_limit.isdigit()):
|
||||||
|
raise ValueError("Memory limit must end with g/m/k or be a number")
|
||||||
Reference in New Issue
Block a user