# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-an-instance-treat-a-tool-result-as-tainted-after-a-prompt-injection-chain-crosses-a-trust-boundary.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-an-instance-treat-a-tool-result-as-tainted-after-a-prompt-injection-chain-crosses-a-trust-boundary.warrant.json
# Markdown SHA-256:   fc7f0d4fa355a688dd2937c4548d7265c48322833a1e376b8c2ddde47e9e3ee6
# Code AST SHA-256:   b8729f11e4bf10d8cb09268feff4ff50e6a6f3aa3663df4e996a9af75310b34d
# Verified Classes:   TaintLaunderingError, ToolManifest, Call
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Set


class TaintLaunderingError(RuntimeError):
    """Raised when a trusted tool would launder taint across a hop."""
    pass


@dataclass
class ToolManifest:
    name: str
    depends_on: Set[str]          # input slots whose values affect the output
    is_sanitizer: bool = False    # verified to break input->output dependence
    has_side_effects: bool = False


@dataclass
class Call:
    tool: ToolManifest
    tainted_slots: Set[str] = field(default_factory=set)


def propagate(call: Call, human_confirmed: bool = False) -> bool:
    """
    Returns True if the output must be labeled tainted.
    Raises TaintLaunderingError if a side-effecting call would proceed
    with tainted input and no human confirmation.
    """
    tainted_inputs = call.tainted_slots & call.tool.depends_on

    if not tainted_inputs:
        return False

    if call.tool.is_sanitizer:
        # Sanitizer verified to break dependence; output is clean.
        return False

    if call.tool.has_side_effects and not human_confirmed:
        raise TaintLaunderingError(
            f"tool '{call.tool.name}' has side effects and receives tainted "
            f"slots {sorted(tainted_inputs)}; human confirmation required"
        )

    # Default: output is causally derived from tainted input -> tainted.
    return True


# Example: the laundering bug that was observed.
summarize = ToolManifest(
    name="summarize",
    depends_on={"body"},
    is_sanitizer=False,   # echoes free text; NOT a sanitizer
    has_side_effects=False,
)
send_invite = ToolManifest(
    name="send_calendar_invite",
    depends_on={"attendee"},
    is_sanitizer=False,
    has_side_effects=True,
)

summary_tainted = propagate(Call(summarize, {"body"}))
assert summary_tainted is True  # must be tainted, not laundered

# Downstream side effect with tainted input and no confirmation must fail.
try:
    propagate(Call(send_invite, {"attendee"}), human_confirmed=False)
except TaintLaunderingError as e:
    print(f"blocked: {e}")
