# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-prompt-injection-taint-has-silently-laundered-across-a-tool-boundary-in-multi-turn-agent-context.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-prompt-injection-taint-has-silently-laundered-across-a-tool-boundary-in-multi-turn-agent-context.warrant.json
# Markdown SHA-256:   8bb21447ce5f6247949f41a1653b4f51c727d0a21348c5f46cda619619ff92fa
# Code AST SHA-256:   69c0764daf1f06594754efeb8b14b4db975717711b9043bf172cb15c66b46293
# Verified Classes:   TaintLaunderingError, Segment, LabelGraph
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass, field


class TaintLaunderingError(RuntimeError):
    """Raised when a segment's label was not derived by a total function
    over all contributing segments' labels."""
    pass


@dataclass
class Segment:
    segment_id: str
    content: bytes
    tainted: bool
    provenance_ns: str

    @property
    def content_hash(self) -> str:
        return hashlib.sha256(self.content).hexdigest()


@dataclass
class LabelGraph:
    segments: dict = field(default_factory=dict)
    # content_hash -> set of provenance namespaces that carried taint
    tainted_hashes: dict = field(default_factory=dict)

    def register(self, seg: Segment) -> None:
        self.segments[seg.segment_id] = seg
        if seg.tainted:
            self.tainted_hashes.setdefault(seg.content_hash, set()).add(seg.provenance_ns)

    def derive(self, inputs: list, output_content: bytes,
               output_ns: str, output_id: str) -> Segment:
        # Total-function requirement: every input must be a registered segment.
        for inp in inputs:
            if inp.segment_id not in self.segments:
                raise TaintLaunderingError(
                    f"input segment {inp.segment_id!r} not registered; "
                    f"label derivation is not total"
                )
        # Conservative union: tainted if any input tainted, OR if this content
        # hash was ever seen tainted under any namespace (cross-modal re-entry).
        h = hashlib.sha256(output_content).hexdigest()
        derived_tainted = any(i.tainted for i in inputs) or (h in self.tainted_hashes)
        if derived_tainted and h not in self.tainted_hashes:
            self.tainted_hashes[h] = set()
        if derived_tainted:
            self.tainted_hashes[h].add(output_ns)
        seg = Segment(output_id, output_content, derived_tainted, output_ns)
        self.register(seg)
        return seg

    def assert_trusted(self, seg: Segment, purpose: str) -> None:
        """Gate for any side-effecting downstream action."""
        if seg.tainted:
            raise TaintLaunderingError(
                f"refusing {purpose!r}: segment {seg.segment_id!r} carries taint "
                f"from namespaces {sorted(self.tainted_hashes.get(seg.content_hash, set()))}"
            )
        # Belt-and-suspenders: if this exact content was ever tainted elsewhere,
        # a clean copy does not launder it.
        if seg.content_hash in self.tainted_hashes:
            raise TaintLaunderingError(
                f"refusing {purpose!r}: content of {seg.segment_id!r} matches a "
                f"previously-tainted hash under {sorted(self.tainted_hashes[seg.content_hash])}"
            )


def apply_tool_output(graph: LabelGraph, inputs: list, tool_return: str,
                      tool_ns: str, out_id: str) -> Segment:
    # Simulate a tool whose schema omitted output_labels: the runtime must
    # still derive conservatively rather than defaulting to clean.
    if tool_return is None:
        raise TaintLaunderingError("tool returned None; cannot derive label")
    return graph.derive(inputs, tool_return.encode(), tool_ns, out_id)


def handle_compaction(graph: LabelGraph, summarized: list, summary: str,
                      out_id: str) -> Segment:
    # Compaction must re-derive from pre-compaction segments, not mint clean.
    return graph.derive(summarized, summary.encode(), "compaction", out_id)
