Create risk_engine.py
Browse files- infrastructure/risk_engine.py +153 -0
infrastructure/risk_engine.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# agentic_reliability_framework/infrastructure/risk_engine.py
|
| 2 |
+
"""
|
| 3 |
+
Risk Scoring Engine – Multi-factor probabilistic risk model.
|
| 4 |
+
|
| 5 |
+
This module computes a risk score (0-1) for an infrastructure intent by combining
|
| 6 |
+
multiple factors with configurable weights. The model is inspired by Bayesian
|
| 7 |
+
decision theory and multi-criteria decision analysis (MCDA). It produces not only
|
| 8 |
+
a score but also a detailed explanation of each factor's contribution, supporting
|
| 9 |
+
transparency and psychological trust.
|
| 10 |
+
|
| 11 |
+
The risk engine is designed to be extended with additional factors (e.g., historical
|
| 12 |
+
data, anomaly scores) without changing the core API.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from typing import Dict, List, Optional, Tuple, Any, Callable
|
| 16 |
+
from dataclasses import dataclass, field
|
| 17 |
+
|
| 18 |
+
from agentic_reliability_framework.infrastructure.intents import (
|
| 19 |
+
InfrastructureIntent,
|
| 20 |
+
ProvisionResourceIntent,
|
| 21 |
+
GrantAccessIntent,
|
| 22 |
+
DeployConfigurationIntent,
|
| 23 |
+
PermissionLevel,
|
| 24 |
+
Environment,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# -----------------------------------------------------------------------------
|
| 28 |
+
# Factor Definition
|
| 29 |
+
# -----------------------------------------------------------------------------
|
| 30 |
+
@dataclass(frozen=True)
|
| 31 |
+
class RiskFactor:
|
| 32 |
+
"""A single factor contributing to risk, with a weight and a scoring function."""
|
| 33 |
+
name: str
|
| 34 |
+
weight: float
|
| 35 |
+
score_fn: Callable[[InfrastructureIntent, Optional[float], List[str]], float]
|
| 36 |
+
description: str = ""
|
| 37 |
+
|
| 38 |
+
def __call__(self, intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 39 |
+
return self.score_fn(intent, cost, violations)
|
| 40 |
+
|
| 41 |
+
# -----------------------------------------------------------------------------
|
| 42 |
+
# Built-in Factors
|
| 43 |
+
# -----------------------------------------------------------------------------
|
| 44 |
+
def intent_type_factor(intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 45 |
+
"""Base risk from intent type."""
|
| 46 |
+
mapping = {
|
| 47 |
+
"provision_resource": 0.1,
|
| 48 |
+
"grant_access": 0.3,
|
| 49 |
+
"deploy_config": 0.2,
|
| 50 |
+
}
|
| 51 |
+
return mapping.get(intent.intent_type, 0.1)
|
| 52 |
+
|
| 53 |
+
def cost_factor(intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 54 |
+
"""Risk contribution from estimated cost (normalized to [0,1])."""
|
| 55 |
+
if not isinstance(intent, ProvisionResourceIntent) or cost is None:
|
| 56 |
+
return 0.0
|
| 57 |
+
# Normalize: $0 → 0, $5000 → 1 (linear)
|
| 58 |
+
return min(cost / 5000.0, 1.0)
|
| 59 |
+
|
| 60 |
+
def permission_factor(intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 61 |
+
"""Risk from permission level being granted."""
|
| 62 |
+
if not isinstance(intent, GrantAccessIntent):
|
| 63 |
+
return 0.0
|
| 64 |
+
mapping = {
|
| 65 |
+
PermissionLevel.READ: 0.1,
|
| 66 |
+
PermissionLevel.WRITE: 0.4,
|
| 67 |
+
PermissionLevel.ADMIN: 0.8,
|
| 68 |
+
}
|
| 69 |
+
return mapping.get(intent.permission_level, 0.5)
|
| 70 |
+
|
| 71 |
+
def scope_factor(intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 72 |
+
"""Risk from deployment scope (for config changes)."""
|
| 73 |
+
if not isinstance(intent, DeployConfigurationIntent):
|
| 74 |
+
return 0.0
|
| 75 |
+
mapping = {
|
| 76 |
+
"single_instance": 0.1,
|
| 77 |
+
"canary": 0.2,
|
| 78 |
+
"global": 0.6,
|
| 79 |
+
}
|
| 80 |
+
return mapping.get(intent.change_scope, 0.3)
|
| 81 |
+
|
| 82 |
+
def environment_factor(intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 83 |
+
"""Additional risk if environment is production."""
|
| 84 |
+
if hasattr(intent, "environment") and intent.environment == Environment.PROD:
|
| 85 |
+
return 0.1
|
| 86 |
+
return 0.0
|
| 87 |
+
|
| 88 |
+
def policy_violation_factor(intent: InfrastructureIntent, cost: Optional[float], violations: List[str]) -> float:
|
| 89 |
+
"""Risk from number of policy violations (capped)."""
|
| 90 |
+
# Each violation adds 0.2, max 0.8
|
| 91 |
+
return min(len(violations) * 0.2, 0.8)
|
| 92 |
+
|
| 93 |
+
# -----------------------------------------------------------------------------
|
| 94 |
+
# Risk Engine
|
| 95 |
+
# -----------------------------------------------------------------------------
|
| 96 |
+
class RiskEngine:
|
| 97 |
+
"""
|
| 98 |
+
Computes a weighted risk score from multiple factors.
|
| 99 |
+
|
| 100 |
+
The engine is initialized with a list of factors and their weights.
|
| 101 |
+
The total score is the weighted sum of factor scores, clamped to [0,1].
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
DEFAULT_FACTORS = [
|
| 105 |
+
RiskFactor("intent_type", 1.0, intent_type_factor, "Base risk from intent type"),
|
| 106 |
+
RiskFactor("cost", 0.3, cost_factor, "Normalized cost estimate"),
|
| 107 |
+
RiskFactor("permission", 0.3, permission_factor, "Permission level being granted"),
|
| 108 |
+
RiskFactor("scope", 0.2, scope_factor, "Deployment scope"),
|
| 109 |
+
RiskFactor("environment", 0.1, environment_factor, "Production environment"),
|
| 110 |
+
RiskFactor("policy_violations", 0.2, policy_violation_factor, "Number of policy violations"),
|
| 111 |
+
]
|
| 112 |
+
|
| 113 |
+
def __init__(self, factors: Optional[List[RiskFactor]] = None):
|
| 114 |
+
"""
|
| 115 |
+
Initialize with custom factors. If none provided, uses DEFAULT_FACTORS.
|
| 116 |
+
"""
|
| 117 |
+
self.factors = factors if factors is not None else self.DEFAULT_FACTORS
|
| 118 |
+
|
| 119 |
+
def calculate_risk(
|
| 120 |
+
self,
|
| 121 |
+
intent: InfrastructureIntent,
|
| 122 |
+
cost_estimate: Optional[float],
|
| 123 |
+
policy_violations: List[str],
|
| 124 |
+
) -> Tuple[float, str, Dict[str, float]]:
|
| 125 |
+
"""
|
| 126 |
+
Compute risk score and detailed breakdown.
|
| 127 |
+
|
| 128 |
+
Returns:
|
| 129 |
+
- total_score: float in [0,1]
|
| 130 |
+
- explanation: human-readable string
|
| 131 |
+
- contributions: dict mapping factor names to their weighted contribution
|
| 132 |
+
"""
|
| 133 |
+
total = 0.0
|
| 134 |
+
contributions = {}
|
| 135 |
+
|
| 136 |
+
for factor in self.factors:
|
| 137 |
+
raw_score = factor(intent, cost_estimate, policy_violations)
|
| 138 |
+
weighted = raw_score * factor.weight
|
| 139 |
+
contributions[factor.name] = weighted
|
| 140 |
+
total += weighted
|
| 141 |
+
|
| 142 |
+
# Clamp to [0,1]
|
| 143 |
+
total = max(0.0, min(total, 1.0))
|
| 144 |
+
|
| 145 |
+
# Build explanation
|
| 146 |
+
lines = [f"Total risk score: {total:.2f}"]
|
| 147 |
+
for factor in self.factors:
|
| 148 |
+
contrib = contributions[factor.name]
|
| 149 |
+
if contrib > 0.0:
|
| 150 |
+
lines.append(f" - {factor.name}: {contrib:.2f} ({factor.description})")
|
| 151 |
+
explanation = "\n".join(lines)
|
| 152 |
+
|
| 153 |
+
return total, explanation, contributions
|