"""The decision function (canon A3). `decide` is pure: it reads its inputs, never mutates them, holds no state and calls no model. The same request, envelope, registry, tenant context and memory always produce the same Decision. """ from __future__ import annotations from typing import Iterable from emergent_security.constitution import PLATFORM_TENANT_BOUND, ROOT from emergent_security.delegation import Delegation, Grant, select_delegation from emergent_security.envelope import DefaultEnvelope, Row from emergent_security.memory import MemoryEntry from emergent_security.model import Decision, Outcome, Request from emergent_security.scales import RiskScales from emergent_security.tenant import TenantContext REQUIRED_SCOPE = ("level", "temperature", "phase", "tenant") def decide( request: Request, *, envelope: DefaultEnvelope, scales: RiskScales, registry: Iterable[Delegation] = (), tenant: TenantContext | None = None, memory: Iterable[MemoryEntry] = (), ) -> Decision: """Evaluate one privileged effect; the first matching step decides.""" def escalate(condition: str, route_to: str, **fields) -> Decision: return Decision(request.request_id, request.decision_class, Outcome.ESCALATE, blocking_condition=condition, route_to=route_to, **fields) # 1. scope unresolved unresolved = _unresolved_scope(request) if unresolved: return escalate("need_context", "delegator", missing=unresolved) row = envelope.row(request.decision_class, request.attributes) # 2. constitution or explicit prohibition if row is not None and row.mode is Outcome.DENY: return _deny(request, row.rules, envelope.id) if tenant is not None and tenant.prohibition(request.decision_class): return _deny(request, tenant.prohibition(request.decision_class), tenant.platform.id) # 3. platform-root if request.scope.get("level") == "L4" or (row is not None and row.mode is Outcome.ROOT_AUTHORIZATION_REQUIRED): return Decision(request.request_id, request.decision_class, Outcome.ROOT_AUTHORIZATION_REQUIRED, route_to="root_steward", rules=(ROOT,), source=envelope.id) # 4. outside the platform tenant envelope if tenant is not None: outside = tenant.outside(request, scales) if outside: return escalate("outside_tenant_envelope", "platform", rules=(PLATFORM_TENANT_BOUND,), missing=outside) route = "platform" if tenant is not None else "delegator" # 5. delegation within the effective envelope grant = _grant(request, row, envelope, registry, tenant, scales) if grant is None: return escalate("no_delegation", route, rules=row.rules if row else ()) # 6. negative memory if any(entry.blocks(request) for entry in memory): return escalate("negative_memory", route, source=grant.source) # 7. evidence missing_evidence = tuple(item for item in grant.evidence_required if item not in request.evidence) if missing_evidence: return escalate("missing_evidence", route, missing=missing_evidence, source=grant.source) # 8. risk against the envelope's declared dimensions exceeded = scales.exceeded(request.risk, grant.risk_envelope) if exceeded: return escalate("risk", route, missing=exceeded, source=grant.source) # 9. tenant reserved the class for a human decision if tenant is not None and tenant.requires_human(request.decision_class): return escalate("tenant_human_decision", "tenant_admin", source=grant.source) # 10. autonomous return Decision(request.request_id, request.decision_class, grant.outcome, rules=grant.rules, obligations=grant.obligations, source=grant.source) def _unresolved_scope(request: Request) -> tuple[str, ...]: missing = [name for name in ("principal", "decision_class") if not getattr(request, name)] missing += [f"scope.{key}" for key in REQUIRED_SCOPE if request.scope.get(key) in (None, "")] return tuple(missing) def _deny(request: Request, rules: tuple[str, ...], source: str) -> Decision: return Decision(request.request_id, request.decision_class, Outcome.DENY, rules=rules, source=source) def _grant( request: Request, row: Row | None, envelope: DefaultEnvelope, registry: Iterable[Delegation], tenant: TenantContext | None, scales: RiskScales, ) -> Grant | None: """Registry first, then the platform tenant envelope, then the default envelope (A10).""" delegation = select_delegation(registry, request) if delegation is not None: grant = delegation.as_grant() elif tenant is not None: grant = tenant.platform.grant_for(request.decision_class) elif row is not None and row.mode is not None and row.mode.grants and envelope.applies_to(request.scope): grant = Grant(source=envelope.id, outcome=row.mode, obligations=row.obligations, evidence_required=row.evidence_required, risk_envelope=envelope.risk_envelope, rules=row.rules) else: return None return grant.bounded_by(tenant.ceilings(scales), scales) if tenant is not None and grant else grant