# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-trust-score-laundering-across-policy-bounded-federated-agent-meshes.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-trust-score-laundering-across-policy-bounded-federated-agent-meshes.warrant.json
# Markdown SHA-256:   540bc9b7d16aad9448eb1574a7d13ed931c1dafc62f440bc47964009df2efde8
# Code AST SHA-256:   ca588b179cf3d3fc59415cfc0772c3b8c66101fbc251aa62bd6c3648cc4b241d
# Verified Classes:   TrustLaunderingError
# Defensive Guards:   3 fail-closed invariants (zero external deps)

class TrustLaunderingError(RuntimeError):
    """Raised when a relayed observation cannot be causally attributed to the asserted originator."""
    pass


def attribute_trust(observation, consumer_policy, edge_scores):
    """
    Attribute a trust observation to its asserted originator, or refuse.

    observation: dict with keys
        'originator'  : asserted principal id
        'relay_chain' : list of principal ids traversed, in order (originator ... relay)
        'signed_by'   : principal that produced the terminal signature
    consumer_policy: callable(principal) -> bool, whether consumer authorizes principal
    edge_scores: dict[(a, b)] -> float in [0,1], independently observed edge trust
    """
    originator = observation["originator"]
    chain = observation["relay_chain"]

    # Condition 3: terminal signature must bind to the originator, not a relay.
    if observation["signed_by"] != originator:
        raise TrustLaunderingError(
            f"terminal signature by {observation['signed_by']!r} does not bind "
            f"originator {originator!r}; relay re-signing breaks causal attribution"
        )

    # Condition 1: every hop must be policy-authorized by the consumer.
    for principal in chain:
        if not consumer_policy(principal):
            raise TrustLaunderingError(
                f"hop principal {principal!r} not authorized by consumer policy"
            )

    # Condition 2: hop-weighted attribution; refuse if any edge is unscored.
    weight = 1.0
    for a, b in zip(chain, chain[1:]):
        edge = edge_scores.get((a, b))
        if edge is None:
            raise TrustLaunderingError(
                f"edge ({a!r}, {b!r}) has no independent trust score; "
                f"refusing to attribute relayed observation to {originator!r}"
            )
        weight *= edge

    return weight
