# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-a-broken-delegation-chain-in-transitive-tool-authorization.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-a-broken-delegation-chain-in-transitive-tool-authorization.warrant.json
# Markdown SHA-256:   07a79177e03468fe32224831d63aa4bdaaa7db20b41fbcf0c9b57b4623402b1f
# Code AST SHA-256:   9b1873132572f34ec6b574a94a8e9517d7af9ed68bebf09d9c592a68a7464d65
# Verified Classes:   DelegationChainError, DelegationToken
# Defensive Guards:   9 fail-closed invariants (zero external deps)

class DelegationChainError(RuntimeError):
    """Raised when a presented delegation chain is structurally valid but
    semantically unsound: broken lineage, missing link, or scope widening."""
    pass


class DelegationToken:
    def __init__(self, jti, parent_jti, subject, issuer, scope, expires_at):
        self.jti = jti
        self.parent_jti = parent_jti
        self.subject = subject
        self.issuer = issuer
        self.scope = frozenset(scope)
        self.expires_at = expires_at


def verify_transitive_chain(tokens, required_scope, now, max_depth=6):
    """Verify a linear delegation chain and return the effective scope.

    Rejects: empty chains, missing links, divergent roots, divergent parent
    lineage across presented tokens, depth overflow, and scope widening.
    """
    if not tokens:
        raise DelegationChainError("empty delegation chain presented")

    if len(tokens) > max_depth:
        raise DelegationChainError(
            f"chain depth {len(tokens)} exceeds max_depth {max_depth}"
        )

    # All presented tokens must share one root subject.
    roots = {t.subject for t in tokens}
    if len(roots) != 1:
        raise DelegationChainError(
            f"divergent root subjects across presented tokens: {sorted(roots)}"
        )

    # Multiple presented tokens must form a single linear ancestor path.
    # A fan-in of sibling tokens is a recombination attempt, not a chain.
    if len(tokens) > 1:
        parent_ids = [t.parent_jti for t in tokens]
        if len(set(parent_ids)) > 1:
            raise DelegationChainError(
                "multiple presented tokens with divergent parent lineage; "
                "recombination of sibling delegations is not permitted"
            )

    # Walk the chain, enforcing contiguous lineage and scope intersection.
    ordered = sorted(tokens, key=lambda t: t.expires_at)
    effective = None
    for i, tok in enumerate(ordered):
        if tok.expires_at <= now:
            raise DelegationChainError(f"token {tok.jti} expired")
        if i > 0:
            prev = ordered[i - 1]
            if tok.parent_jti != prev.jti:
                raise DelegationChainError(
                    f"broken lineage: token {tok.jti} claims parent "
                    f"{tok.parent_jti} but preceding token is {prev.jti}"
                )
        effective = tok.scope if effective is None else (effective & tok.scope)

    if effective is None:
        raise DelegationChainError("no effective scope computed")

    # Widening detection: the leaf must not assert more than the intersection.
    leaf = ordered[-1]
    if not leaf.scope <= effective:
        raise DelegationChainError(
            f"scope widening at leaf {leaf.jti}: "
            f"{sorted(leaf.scope - effective)} not granted by ancestors"
        )

    if not required_scope <= effective:
        raise DelegationChainError(
            f"required scope {sorted(required_scope)} not covered by "
            f"effective scope {sorted(effective)}"
        )

    return effective
