# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-delegated-credential-confusion-in-aaif-agent-identity-issuance-and-revocation.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-delegated-credential-confusion-in-aaif-agent-identity-issuance-and-revocation.warrant.json
# Markdown SHA-256:   2ed6435ba6d4c6616738877d841c6bfadc6c39318ed32638a94be3b9c1842fe9
# Code AST SHA-256:   38d3b7e0a3ad12ecbc87597f3ffcbd3c60ea611fefa4b58101acfb9c11749355
# Verified Classes:   DelegationRevocationError, Credential, Revocation
# Defensive Guards:   2 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass


class DelegationRevocationError(RuntimeError):
    """Raised when a revocation record cannot safely target a delegated credential."""
    pass


@dataclass(frozen=True)
class Credential:
    sub: str                 # on whose behalf
    act: str                 # who is acting
    issuer_chain: tuple[str, ...]  # ordered issuer IDs, root first
    exp: int

    @property
    def path_hash(self) -> str:
        h = hashlib.sha256()
        for issuer in self.issuer_chain:
            h.update(issuer.encode())
            h.update(b"\x00")
        h.update(self.sub.encode())
        h.update(b"\x00")
        h.update(self.act.encode())
        return h.hexdigest()


@dataclass(frozen=True)
class Revocation:
    key: str                 # bare principal ID, or a path hash
    path_qualified: bool


def enforce_revocation(rev: Revocation, target: Credential, is_issuer_in_live_chain: bool) -> None:
    if is_issuer_in_live_chain and not rev.path_qualified:
        raise DelegationRevocationError(
            f"revocation key={rev.key!r} is not path-qualified but target "
            f"credential path={target.path_hash} issues downstream credentials; "
            f"principal-keyed revocation would leak authority for up to the "
            f"downstream TTL ({target.exp}). Reject and reissue as a path-keyed "
            f"revocation over issuer_chain={target.issuer_chain!r}."
        )
    if rev.path_qualified and rev.key != target.path_hash and not target.path_hash.startswith(rev.key):
        raise DelegationRevocationError(
            f"path-keyed revocation key={rev.key!r} does not prefix-match "
            f"target path={target.path_hash}; refusing to apply a revocation "
            f"that names a different delegation subtree."
        )
    # Revocation is well-formed for this target; caller proceeds to invalidate.
