# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/why-does-an-instance-lose-prompt-injection-taint-when-a-tool-re-serializes-retrieved-context.md
# Remnant Warrant:    https://corpusfetch.com/c/why-does-an-instance-lose-prompt-injection-taint-when-a-tool-re-serializes-retrieved-context.warrant.json
# Markdown SHA-256:   f4fa39cd2db1296c933ec9471bb031fc9b7dc1284350e18b091f984e20864c8b
# Code AST SHA-256:   9fc566e417b1e049e64af2fd190c87d920e83bc58dd74dba36d986e3ceb09a45
# Verified Classes:   Taint, TaintLaunderingError, Span
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from enum import IntEnum
from typing import Optional


class Taint(IntEnum):
    TRUSTED = 0
    UNTRUSTED_RETRIEVED = 1
    UNTRUSTED_TOOL_OUTPUT = 2

    def join(self, other: "Taint") -> "Taint":
        # Least-upper-bound in trust order: the more-tainted label wins.
        return Taint(max(int(self), int(other)))


class TaintLaunderingError(RuntimeError):
    """Raised when a trusted container is about to carry untrusted bytes without inheriting taint."""


@dataclass(frozen=True)
class Span:
    text: str
    taint: Taint
    origin: Optional[str] = None


def _normalize(s: str) -> str:
    return " ".join(s.split()).lower()


def resolve_tool_return(
    tool_output: Span,
    previously_tainted: list[Span],
    tool_trust: Taint = Taint.TRUSTED,
) -> Span:
    """
    Enforce: container taint = join(tool_trust, taint of every embedded untrusted span).
    Verbatim-or-near-verbatim overlap is treated as transport (fail closed).
    """
    out_norm = _normalize(tool_output.text)
    inherited = tool_trust
    for src in previously_tainted:
        if src.taint == Taint.TRUSTED:
            continue
        needle = _normalize(src.text)
        if needle and needle in out_norm:
            # Transport, not transform: inherit the source's taint.
            inherited = inherited.join(src.taint)
            # A trusted tool must not silently sanitize by wrapping.
            if tool_trust == Taint.TRUSTED and src.taint != Taint.TRUSTED:
                if tool_output.taint == Taint.TRUSTED:
                    raise TaintLaunderingError(
                        f"tool output labeled {tool_output.taint.name} embeds "
                        f"{src.taint.name} bytes from {src.origin!r}; "
                        f"container taint must be >= {inherited.name}"
                    )
    return Span(text=tool_output.text, taint=inherited, origin=tool_output.origin)


def plan_action(span: Span, action: str) -> str:
    # Any control-flow decision driven by an untrusted span is refused.
    if span.taint != Taint.TRUSTED:
        raise TaintLaunderingError(
            f"refusing action {action!r}: driven by {span.taint.name} span, not TRUSTED"
        )
    return action
