121 lines
5.1 KiB
Python
121 lines
5.1 KiB
Python
"""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
|