# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-cross-vendor-agent-authentication-failures-under-aaif-attestation.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-cross-vendor-agent-authentication-failures-under-aaif-attestation.warrant.json
# Markdown SHA-256:   9a8081568ce755b177ec385b863e428a5a24e3fe298ba130fa0d23f2c3486d0a
# Code AST SHA-256:   3efc92da1b0b356b78985e464e4c2c7a94a617c1979d74f005d91ea4617b4430
# Verified Classes:   AuthorityScopeGap, Certificate, Attestation
# Defensive Guards:   4 fail-closed invariants (zero external deps)

"""Authority-scope intersection check for AAIF attestation chains.

Operational guard: a cryptographically valid attestation is insufficient for
authorization. Every authorization-driving claim must be covered by the
authority-scope extension of *every* certificate in the chain, evaluated at
decision time.
"""

from dataclasses import dataclass
from typing import FrozenSet


class AuthorityScopeGap(RuntimeError):
    """Raised when an asserted claim is not covered by delegated authority."""


@dataclass(frozen=True)
class Certificate:
    subject: str
    authority_scope: FrozenSet[str]  # claim classes this cert may assert/delegate
    not_after: float


@dataclass(frozen=True)
class Attestation:
    asserted_claims: FrozenSet[str]
    claim_not_after: float
    chain: tuple  # leaf -> ... -> root


MUTATING_CAPABILITIES = frozenset({"write", "delete", "transfer", "residency-cross"})


def verify_authority(
    attestation: Attestation,
    requested_capability: str,
    now: float,
) -> None:
    """Enforce the authority-scope intersection invariant.

    Raises AuthorityScopeGap on any gap for mutating capabilities; permits a
    logged degraded posture for read-only capabilities only.
    """
    if not attestation.chain:
        raise AuthorityScopeGap("empty attestation chain")

    # Invariant 4: claim freshness must be strictly bounded by authority lifetime.
    for cert in attestation.chain:
        if attestation.claim_not_after >= cert.not_after:
            raise AuthorityScopeGap(
                f"claim lifetime {attestation.claim_not_after} not bounded by "
                f"cert {cert.subject} not_after {cert.not_after}"
            )

    # Invariant 1 & 2: per-decision intersection across the whole chain.
    covered = frozenset.intersection(
        *(cert.authority_scope for cert in attestation.chain)
    )
    uncovered = attestation.asserted_claims - covered

    if uncovered:
        if requested_capability in MUTATING_CAPABILITIES:
            raise AuthorityScopeGap(
                f"capability {requested_capability!r} requires claims "
                f"{sorted(uncovered)} not covered by chain authority"
            )
        # Read-only: proceed in explicitly recorded degraded posture.
        return

    if now > attestation.claim_not_after:
        raise AuthorityScopeGap("attestation claims expired at decision time")
