Files
syndarix/backend/app/services/safety/config.py
Felipe Cardoso 520c06175e refactor(safety): apply consistent formatting across services and tests
Improved code readability and uniformity by standardizing line breaks, indentation, and inline conditions across safety-related services, models, and tests, including content filters, validation rules, and emergency controls.
2026-01-03 16:23:39 +01:00

305 lines
11 KiB
Python

"""
Safety Framework Configuration
Pydantic settings for the safety and guardrails framework.
"""
import logging
import os
from functools import lru_cache
from pathlib import Path
from typing import Any
import yaml
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from .models import AutonomyLevel, SafetyPolicy
logger = logging.getLogger(__name__)
class SafetyConfig(BaseSettings):
"""Configuration for the safety framework."""
model_config = SettingsConfigDict(
env_prefix="SAFETY_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# General settings
enabled: bool = Field(True, description="Enable safety framework")
strict_mode: bool = Field(True, description="Strict mode (fail closed on errors)")
log_level: str = Field("INFO", description="Logging level")
# Default autonomy level
default_autonomy_level: AutonomyLevel = Field(
AutonomyLevel.MILESTONE,
description="Default autonomy level for new agents",
)
# Default budget limits
default_session_token_budget: int = Field(
100_000, description="Default tokens per session"
)
default_daily_token_budget: int = Field(
1_000_000, description="Default tokens per day"
)
default_session_cost_limit: float = Field(
10.0, description="Default USD per session"
)
default_daily_cost_limit: float = Field(100.0, description="Default USD per day")
# Default rate limits
default_actions_per_minute: int = Field(60, description="Default actions per min")
default_llm_calls_per_minute: int = Field(20, description="Default LLM calls/min")
default_file_ops_per_minute: int = Field(100, description="Default file ops/min")
# Loop detection
loop_detection_enabled: bool = Field(True, description="Enable loop detection")
max_repeated_actions: int = Field(5, description="Max exact repetitions")
max_similar_actions: int = Field(10, description="Max similar actions")
loop_history_size: int = Field(100, description="Action history size for loops")
# HITL settings
hitl_enabled: bool = Field(True, description="Enable human-in-the-loop")
hitl_default_timeout: int = Field(300, description="Default approval timeout (s)")
hitl_notification_channels: list[str] = Field(
default_factory=list, description="Notification channels"
)
# Rollback settings
rollback_enabled: bool = Field(True, description="Enable rollback capability")
checkpoint_dir: str = Field(
"/tmp/syndarix_checkpoints", # noqa: S108
description="Directory for checkpoint storage",
)
checkpoint_retention_hours: int = Field(24, description="Checkpoint retention")
auto_checkpoint_destructive: bool = Field(
True, description="Auto-checkpoint destructive actions"
)
# Sandbox settings
sandbox_enabled: bool = Field(False, description="Enable sandbox execution")
sandbox_timeout: int = Field(300, description="Sandbox timeout (s)")
sandbox_memory_mb: int = Field(1024, description="Sandbox memory limit (MB)")
sandbox_cpu_limit: float = Field(1.0, description="Sandbox CPU limit")
sandbox_network_enabled: bool = Field(False, description="Allow sandbox network")
# Audit settings
audit_enabled: bool = Field(True, description="Enable audit logging")
audit_retention_days: int = Field(90, description="Audit log retention (days)")
audit_include_sensitive: bool = Field(
False, description="Include sensitive data in audit"
)
# Content filtering
content_filter_enabled: bool = Field(True, description="Enable content filtering")
filter_pii: bool = Field(True, description="Filter PII")
filter_secrets: bool = Field(True, description="Filter secrets")
# Emergency controls
emergency_stop_enabled: bool = Field(True, description="Enable emergency stop")
emergency_webhook_url: str | None = Field(None, description="Emergency webhook")
# Policy file path
policy_file: str | None = Field(None, description="Path to policy YAML file")
# Validation cache
validation_cache_ttl: int = Field(60, description="Validation cache TTL (s)")
validation_cache_size: int = Field(1000, description="Validation cache size")
class AutonomyConfig(BaseSettings):
"""Configuration for autonomy levels."""
model_config = SettingsConfigDict(
env_prefix="AUTONOMY_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
# FULL_CONTROL settings
full_control_cost_limit: float = Field(1.0, description="USD limit per session")
full_control_require_all_approval: bool = Field(
True, description="Require approval for all"
)
full_control_block_destructive: bool = Field(
True, description="Block destructive actions"
)
# MILESTONE settings
milestone_cost_limit: float = Field(10.0, description="USD limit per session")
milestone_require_critical_approval: bool = Field(
True, description="Require approval for critical"
)
milestone_auto_checkpoint: bool = Field(
True, description="Auto-checkpoint destructive"
)
# AUTONOMOUS settings
autonomous_cost_limit: float = Field(100.0, description="USD limit per session")
autonomous_auto_approve_normal: bool = Field(
True, description="Auto-approve normal actions"
)
autonomous_auto_checkpoint: bool = Field(True, description="Auto-checkpoint all")
def _expand_env_vars(value: Any) -> Any:
"""Recursively expand environment variables in values."""
if isinstance(value, str):
return os.path.expandvars(value)
elif isinstance(value, dict):
return {k: _expand_env_vars(v) for k, v in value.items()}
elif isinstance(value, list):
return [_expand_env_vars(v) for v in value]
return value
def load_policy_from_file(file_path: str | Path) -> SafetyPolicy | None:
"""Load a safety policy from a YAML file."""
path = Path(file_path)
if not path.exists():
logger.warning("Policy file not found: %s", path)
return None
try:
with open(path) as f:
data = yaml.safe_load(f)
if data is None:
logger.warning("Empty policy file: %s", path)
return None
# Expand environment variables
data = _expand_env_vars(data)
return SafetyPolicy(**data)
except Exception as e:
logger.error("Failed to load policy file %s: %s", path, e)
return None
def load_policies_from_directory(directory: str | Path) -> dict[str, SafetyPolicy]:
"""Load all safety policies from a directory."""
policies: dict[str, SafetyPolicy] = {}
path = Path(directory)
if not path.exists() or not path.is_dir():
logger.warning("Policy directory not found: %s", path)
return policies
for file_path in path.glob("*.yaml"):
policy = load_policy_from_file(file_path)
if policy:
policies[policy.name] = policy
logger.info("Loaded policy: %s from %s", policy.name, file_path.name)
return policies
@lru_cache(maxsize=1)
def get_safety_config() -> SafetyConfig:
"""Get the safety configuration (cached singleton)."""
return SafetyConfig()
@lru_cache(maxsize=1)
def get_autonomy_config() -> AutonomyConfig:
"""Get the autonomy configuration (cached singleton)."""
return AutonomyConfig()
def get_default_policy() -> SafetyPolicy:
"""Get the default safety policy."""
config = get_safety_config()
return SafetyPolicy(
name="default",
description="Default safety policy",
max_tokens_per_session=config.default_session_token_budget,
max_tokens_per_day=config.default_daily_token_budget,
max_cost_per_session_usd=config.default_session_cost_limit,
max_cost_per_day_usd=config.default_daily_cost_limit,
max_actions_per_minute=config.default_actions_per_minute,
max_llm_calls_per_minute=config.default_llm_calls_per_minute,
max_file_operations_per_minute=config.default_file_ops_per_minute,
max_repeated_actions=config.max_repeated_actions,
max_similar_actions=config.max_similar_actions,
require_sandbox=config.sandbox_enabled,
sandbox_timeout_seconds=config.sandbox_timeout,
sandbox_memory_mb=config.sandbox_memory_mb,
)
def get_policy_for_autonomy_level(level: AutonomyLevel) -> SafetyPolicy:
"""Get the safety policy for a given autonomy level."""
autonomy = get_autonomy_config()
base_policy = get_default_policy()
if level == AutonomyLevel.FULL_CONTROL:
return SafetyPolicy(
name="full_control",
description="Full control mode - all actions require approval",
max_cost_per_session_usd=autonomy.full_control_cost_limit,
max_cost_per_day_usd=autonomy.full_control_cost_limit * 10,
require_approval_for=["*"], # All actions
max_tokens_per_session=base_policy.max_tokens_per_session // 10,
max_tokens_per_day=base_policy.max_tokens_per_day // 10,
max_actions_per_minute=base_policy.max_actions_per_minute // 2,
max_llm_calls_per_minute=base_policy.max_llm_calls_per_minute // 2,
max_file_operations_per_minute=base_policy.max_file_operations_per_minute
// 2,
denied_tools=["delete_*", "destroy_*", "drop_*"],
)
elif level == AutonomyLevel.MILESTONE:
return SafetyPolicy(
name="milestone",
description="Milestone mode - approval at milestones only",
max_cost_per_session_usd=autonomy.milestone_cost_limit,
max_cost_per_day_usd=autonomy.milestone_cost_limit * 10,
require_approval_for=[
"delete_file",
"push_to_remote",
"deploy_*",
"modify_critical_*",
"create_pull_request",
],
max_tokens_per_session=base_policy.max_tokens_per_session,
max_tokens_per_day=base_policy.max_tokens_per_day,
max_actions_per_minute=base_policy.max_actions_per_minute,
max_llm_calls_per_minute=base_policy.max_llm_calls_per_minute,
max_file_operations_per_minute=base_policy.max_file_operations_per_minute,
)
else: # AUTONOMOUS
return SafetyPolicy(
name="autonomous",
description="Autonomous mode - minimal intervention",
max_cost_per_session_usd=autonomy.autonomous_cost_limit,
max_cost_per_day_usd=autonomy.autonomous_cost_limit * 10,
require_approval_for=[
"deploy_to_production",
"delete_repository",
"modify_production_config",
],
max_tokens_per_session=base_policy.max_tokens_per_session * 5,
max_tokens_per_day=base_policy.max_tokens_per_day * 5,
max_actions_per_minute=base_policy.max_actions_per_minute * 2,
max_llm_calls_per_minute=base_policy.max_llm_calls_per_minute * 2,
max_file_operations_per_minute=base_policy.max_file_operations_per_minute
* 2,
)
def reset_config_cache() -> None:
"""Reset configuration caches (for testing)."""
get_safety_config.cache_clear()
get_autonomy_config.cache_clear()