# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-prompt-injection-taint-that-survives-tool-call-graph-propagation.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-prompt-injection-taint-that-survives-tool-call-graph-propagation.warrant.json
# Markdown SHA-256:   33099bd4338238eaeda512e1bf18dcbd98efb43101cbb4c9ad8ef5df6b787668
# Code AST SHA-256:   8a6ea1cfba02a3add979cfe8fed5666af15855a8db27a482e1de5cd2a2cf45df
# Verified Classes:   TaintLaunderingError
# Defensive Guards:   1 fail-closed invariants (zero external deps)

class TaintLaunderingError(RuntimeError):
    """Raised when a tool call reaches a privileged sink with content derived
    from a tainted span but carrying a clean provenance label."""
    pass


def check_derivation_coverage(call, label_graph, derivation_graph, privileged_sinks):
    """Enforce derivation ⊆ label-closure for a single tool call.

    call: dict with keys 'tool', 'args', 'label', 'derivation_parents'
    label_graph: mapping node_id -> set(node_id) of labeled edges
    derivation_graph: mapping node_id -> set(node_id) of content-flow edges
    privileged_sinks: set of tool names with side effects
    """
    if call["tool"] not in privileged_sinks:
        return True  # non-privileged: record only, no block

    # Walk derivation ancestry of the call's arguments.
    tainted_ancestors = set()
    frontier = list(call.get("derivation_parents", []))
    seen = set(frontier)
    while frontier:
        node = frontier.pop()
        if node in label_graph and label_graph[node]:
            # node has an incoming taint label
            tainted_ancestors.add(node)
        for parent in derivation_graph.get(node, ()):
            if parent not in seen:
                seen.add(parent)
                frontier.append(parent)

    if not tainted_ancestors:
        return True  # no derivation path from any tainted node

    # A tainted derivation exists; the call must be labeled tainted too.
    if not call["label"].startswith("untrusted"):
        raise TaintLaunderingError(
            f"Tool {call['tool']!r} reached privileged sink with clean label "
            f"{call['label']!r}, but derivation graph shows taint from "
            f"{sorted(tainted_ancestors)}. Laundered injection suspected."
        )
    return True
