# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-refuse-to-extend-a-delegation-chain-across-a-federation-hop-despite-a-valid-signature-the-authority-scope-narrowing-invariant-in.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-refuse-to-extend-a-delegation-chain-across-a-federation-hop-despite-a-valid-signature-the-authority-scope-narrowing-invariant-in.warrant.json
# Markdown SHA-256:   572c0af9216ff4db30673a8374698cf6f4b8e740efe20503738bd001fc636387
# Code AST SHA-256:   be49cda4a9605e2e4c8fcce90947817b034a8599a41c9a640f74ea08cc010da9
# Verified Classes:   UnprovableDelegationCeiling, Scope
# Defensive Guards:   4 fail-closed invariants (zero external deps)

class UnprovableDelegationCeiling(RuntimeError):
    """Raised when a federation hop cannot prove its sub-delegation ceiling
    is a subset of the delegating principal's ceiling. The chain must not
    be extended past this hop, though the token may be honored as terminal."""
    pass


class Scope:
    def __init__(self, grants, ceiling=None):
        self.grants = frozenset(grants)
        self.ceiling = frozenset(ceiling) if ceiling is not None else None

    def is_subset_of(self, other):
        return self.grants <= other.grants


def validate_hop(hop, prior_hop, ceiling_resolvable: bool):
    """
    hop: dict with keys issuer, scope(Scope), is_root(bool)
    prior_hop: dict or None
    ceiling_resolvable: whether the issuer's subdelegate_ceiling is
        attestable from the validating federation's authority store.
    """
    if hop["is_root"]:
        # Root ceiling is its full scope; no cross-hop proof needed.
        return "accept-and-extend"

    if prior_hop is None:
        raise UnprovableDelegationCeiling(
            "non-root hop with no prior hop: chain is malformed"
        )

    # 1. Scope must narrow.
    if not hop["scope"].is_subset_of(prior_hop["scope"]):
        raise UnprovableDelegationCeiling(
            f"scope widened at hop: {hop['scope'].grants} not subset of "
            f"{prior_hop['scope'].grants}"
        )

    # 2. Ceiling must be resolvable across the federation boundary.
    if not ceiling_resolvable or hop["scope"].ceiling is None:
        # Cannot prove ceiling(hop) <= ceiling(prior). Do not extend.
        raise UnprovableDelegationCeiling(
            "cross-federation hop: sub-delegation ceiling unresolvable; "
            "refusing to extend chain (token may be honored as terminal only)"
        )

    prior_ceiling = prior_hop["scope"].ceiling or prior_hop["scope"].grants
    if not (hop["scope"].ceiling <= prior_ceiling):
        raise UnprovableDelegationCeiling(
            f"ceiling widened at hop: {hop['scope'].ceiling} not subset of "
            f"{prior_ceiling}"
        )

    # 3. If the token requests downstream sub-delegation beyond its scope,
    #    truncate rather than extend.
    if hop["scope"].ceiling - hop["scope"].grants:
        return "accept-but-truncate"

    return "accept-and-extend"
