emergent-security/src/emergent_security/model.py

107 lines
3.2 KiB
Python

"""Pure data types shared by every module: the request, the decision and its outcome."""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from types import MappingProxyType
from typing import Any, Iterable, Mapping
def same_value(left: Any, right: Any) -> bool:
"""Type-strict equality, so that YAML `true` never matches `1` or `"true"`."""
return type(left) is type(right) and left == right
def frozen_mapping(value: Mapping[str, Any] | None) -> Mapping[str, Any]:
return MappingProxyType(dict(value or {}))
def frozen_tuple(value: Iterable[str] | None) -> tuple[str, ...]:
return tuple(value or ())
class Outcome(str, Enum):
AUTONOMOUS = "AUTONOMOUS"
AUTONOMOUS_WITH_OBLIGATIONS = "AUTONOMOUS_WITH_OBLIGATIONS"
ESCALATE = "ESCALATE"
DENY = "DENY"
ROOT_AUTHORIZATION_REQUIRED = "ROOT_AUTHORIZATION_REQUIRED"
@classmethod
def from_mode(cls, mode: Any) -> Outcome | None:
"""Map an envelope `mode` string to an Outcome; None for unknown modes."""
try:
return cls(str(mode).upper())
except ValueError:
return None
@property
def grants(self) -> bool:
return self in (Outcome.AUTONOMOUS, Outcome.AUTONOMOUS_WITH_OBLIGATIONS)
@property
def effect(self) -> str:
if self.grants:
return "allow"
if self is Outcome.DENY:
return "deny"
return "escalate"
@dataclass(frozen=True)
class Request:
"""A privileged effect an agent intends, described independently of any credential (A4)."""
request_id: str
principal: str | None
decision_class: str | None
scope: Mapping[str, Any]
attributes: Mapping[str, Any]
risk: Mapping[str, Any]
evidence: frozenset[str]
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> Request:
return cls(
request_id=str(data.get("request_id", "")),
principal=data.get("principal"),
decision_class=data.get("decision_class"),
scope=frozen_mapping(data.get("scope")),
attributes=frozen_mapping(data.get("attributes")),
risk=frozen_mapping(data.get("risk")),
evidence=frozenset(data.get("evidence") or ()),
)
@dataclass(frozen=True)
class Decision:
"""Decision record (canon B2). `missing` lists what stands between the request and an allow."""
request_id: str
decision_class: str | None
outcome: Outcome
blocking_condition: str | None = None
route_to: str | None = None
rules: tuple[str, ...] = ()
obligations: tuple[str, ...] = ()
missing: tuple[str, ...] = ()
source: str | None = None
@property
def effect(self) -> str:
return self.outcome.effect
def to_dict(self) -> dict[str, Any]:
return {
"request_id": self.request_id,
"decision_class": self.decision_class,
"outcome": self.outcome.value,
"effect": self.effect,
"blocking_condition": self.blocking_condition,
"route_to": self.route_to,
"rules": list(self.rules),
"obligations": list(self.obligations),
"missing": list(self.missing),
"source": self.source,
}