# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-re-validate-a-delegated-credential-chain-the-attenuation-gap-at-the-intersection-of-scope-intersection-and-temporal-re-binding.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-re-validate-a-delegated-credential-chain-the-attenuation-gap-at-the-intersection-of-scope-intersection-and-temporal-re-binding.warrant.json
# Markdown SHA-256:   c5d90931ac37ddaba047d2289a95a3164c288483cd270142719307f678f0f6ad
# Code AST SHA-256:   91d356000b1e3a7cc404da60f02e45f409b9dc852772ea79c564ab269c93e139
# Verified Classes:   ChainRevalidationError, Hop, DelegatedChain
# Defensive Guards:   6 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import FrozenSet


class ChainRevalidationError(RuntimeError):
    """Raised when a delegated credential chain fails use-time re-validation."""


@dataclass(frozen=True)
class Hop:
    agent_id: str
    scope: FrozenSet[str]
    issued_at: float
    expires_at: float
    revoked: bool = False


@dataclass
class DelegatedChain:
    hops: list = field(default_factory=list)  # root first, leaf last

    def effective_scope_at(self, now: float, skew_budget: float = 0.0) -> FrozenSet[str]:
        """Re-validate the whole chain at `now` and return the attenuated scope.

        The intersection is computed only after every hop validates. A cached
        scope is never accepted here; callers must pass the live chain.
        """
        if not self.hops:
            raise ChainRevalidationError("empty chain: no root to anchor trust")

        # 1. Every hop must be live at `now`, not just the leaf. An ancestor
        #    that expired or was revoked invalidates the whole subtree.
        for i, hop in enumerate(self.hops):
            if hop.revoked:
                raise ChainRevalidationError(
                    f"hop {i} ({hop.agent_id}) is revoked; leaf scope is stale"
                )
            if now >= hop.expires_at + skew_budget:
                raise ChainRevalidationError(
                    f"hop {i} ({hop.agent_id}) expired at {hop.expires_at}, now={now}"
                )
            if now < hop.issued_at - skew_budget:
                raise ChainRevalidationError(
                    f"hop {i} ({hop.agent_id}) not yet valid (issued {hop.issued_at})"
                )

        # 2. Attenuation must be monotone along the chain: each child's scope
        #    is a subset of its parent's. A violation means the chain was
        #    forged or a delegation was miscomputed.
        scope = self.hops[0].scope
        for i in range(1, len(self.hops)):
            child = self.hops[i].scope
            if not child.issubset(self.hops[i - 1].scope):
                raise ChainRevalidationError(
                    f"hop {i} ({self.hops[i].agent_id}) escalates beyond parent: "
                    f"{sorted(child - self.hops[i - 1].scope)}"
                )
            scope = scope & child

        return scope


def authorize(chain: DelegatedChain, action: str, now: float) -> bool:
    """Authorize a single action by re-validating the live chain at use time."""
    try:
        scope = chain.effective_scope_at(now)
    except ChainRevalidationError as exc:
        # Fail closed: a chain we cannot re-validate grants nothing.
        raise ChainRevalidationError(f"authorization denied: {exc}") from exc
    return action in scope
