# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-propagate-a-taint-label-across-a-tool-call-boundary-that-is-not-a-data-edge.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-propagate-a-taint-label-across-a-tool-call-boundary-that-is-not-a-data-edge.warrant.json
# Markdown SHA-256:   1a7dcfa379516421117dcd0759d7cc6ecc9e15a164d2f816180832fbf2948b07
# Code AST SHA-256:   3e53d18c3d77b76039b100c2a536e8bdc1a95e059a965a6bae2807ddfa8b71d2
# Verified Classes:   TaintPropagationError, ToolCall
# Defensive Guards:   2 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import Any

class TaintPropagationError(RuntimeError):
    """Raised when a side-effecting tool call is reachable from tainted context
    via a control edge that the data-edge propagation rule did not cover."""
    pass

@dataclass
class ToolCall:
    name: str
    args: dict[str, Any]
    control_tainted: bool = False   # planner consumed tainted context
    arg_tainted: bool = False       # any argument derived from tainted value

SIDE_EFFECTING = {"execute_payment", "transfer_funds", "send_email"}

def gate_tool_call(call: ToolCall, allow_control_taint: bool = False) -> None:
    if call.name not in SIDE_EFFECTING:
        return
    if call.arg_tainted:
        raise TaintPropagationError(
            f"tool {call.name!r} has tainted arguments; explicit-flow violation"
        )
    if call.control_tainted and not allow_control_taint:
        # The non-obvious case: constants look clean, but the *decision*
        # to call was downstream of untrusted input (implicit flow).
        raise TaintPropagationError(
            f"tool {call.name!r} invoked from tainted control context; "
            f"implicit-flow violation (args={call.args!r} appear clean)"
        )

def run_agent_step(planner_context_tainted: bool, emitted: ToolCall) -> None:
    # Propagate the control label from planner context onto the emitted call.
    emitted.control_tainted = emitted.control_tainted or planner_context_tainted
    gate_tool_call(emitted)
