"""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, }