# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-delegation-chain-has-silently-lost-attenuation-the-transitive-confused-deputy-boundary-in-agent-capability-graphs.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-delegation-chain-has-silently-lost-attenuation-the-transitive-confused-deputy-boundary-in-agent-capability-graphs.warrant.json
# Markdown SHA-256:   7ce8d92391521b92f4a07b9d0a2ae20dad380bf89e99a79391f0b7d5bd73277c
# Code AST SHA-256:   7ac4cbd82a30ba4c929ce159b66714e8eca9480d82721ad5ede026fb4c68d749
# Verified Classes:   AttenuationUnverifiedError
# Defensive Guards:   5 fail-closed invariants (zero external deps)

class AttenuationUnverifiedError(RuntimeError):
    """Raised when a delegation chain's attenuation cannot be verified at use time."""
    pass


def verify_attenuation_chain(chain, resolver):
    """
    chain: list of hops, root -> leaf. Each hop: {"cap_id", "scopes", "parent_cap_id", "parent_sig"}.
    resolver: object exposing .resolve(cap_id) -> set[str] and .cache_key(cap_id) -> str.
    Returns the effective scope set if the chain is sound; raises otherwise.
    """
    if not chain:
        raise AttenuationUnverifiedError("empty delegation chain")

    effective = set(resolver.resolve(chain[0]["cap_id"]))

    for i, hop in enumerate(chain[1:], start=1):
        parent = chain[i - 1]

        # Parent link must be grantor-signed and point at the actual parent capability.
        if hop.get("parent_cap_id") != parent["cap_id"]:
            raise AttenuationUnverifiedError(
                f"hop {i}: parent link {hop.get('parent_cap_id')!r} "
                f"does not match actual parent {parent['cap_id']!r} (possible laundering)"
            )
        if not hop.get("parent_sig"):
            raise AttenuationUnverifiedError(
                f"hop {i}: parent link is unsigned; chain-shortening cannot be ruled out"
            )

        # Cache key must be capability-identity-keyed, not resource-keyed.
        if resolver.cache_key(hop["cap_id"]) == resolver.cache_key(parent["cap_id"]):
            raise AttenuationUnverifiedError(
                f"hop {i}: resolver cache key collides with parent; "
                f"ambient-authority channel present"
            )

        hop_scopes = set(resolver.resolve(hop["cap_id"]))
        if not hop_scopes.issubset(effective):
            raise AttenuationUnverifiedError(
                f"hop {i}: scopes {hop_scopes - effective} exceed ancestor authority "
                f"{effective} (attenuation violated at use time)"
            )
        effective &= hop_scopes

    return effective
