# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-a-runtime-policy-enforcer-reject-a-tool-call-whose-function-name-is-allowlisted-but-whose-arguments-carry-tain.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-a-runtime-policy-enforcer-reject-a-tool-call-whose-function-name-is-allowlisted-but-whose-arguments-carry-tain.warrant.json
# Markdown SHA-256:   8419ed2e8854b094e9da39ad3cb727543f9f20db13843ea982ede6bfbc75e24f
# Code AST SHA-256:   78e681efd01566d22ecdbc0d9386ec263606bbe37fda7edfcc96b0e47b6b9ed8
# Verified Classes:   TaintViolation, AllowlistViolation
# Defensive Guards:   2 fail-closed invariants (zero external deps)

class TaintViolation(RuntimeError):
    """Raised when a tainted value reaches a sink argument of an allowlisted tool."""
    pass


class AllowlistViolation(RuntimeError):
    """Raised when a tool is not on the allowlist."""
    pass


# tool -> set of argument positions that are sinks
SINK_POSITIONS = {
    "run_sql": {"query"},
    "http_get": {"url"},
    "send_email": {"body", "to"},
    "read_file": {"path"},
}

ALLOWLIST = set(SINK_POSITIONS) | {"log"}

# (tool, position) -> sanitizer that discharges taint
REGISTERED_SANITIZERS = {
    ("run_sql", "query"): "sql_escape",
    ("http_get", "url"): "url_allowlist",
}


def enforce(tool: str, args: dict, taint: dict) -> None:
    # Step 1: allowlist gate. Denial here is a capability denial.
    if tool not in ALLOWLIST:
        raise AllowlistViolation(f"tool {tool!r} not allowlisted")

    # Steps 2-3: taint gate, evaluated as a SIBLING, never beneath step 1.
    for pos in SINK_POSITIONS.get(tool, set()):
        if pos not in args:
            continue
        label = taint.get(pos, "CLEAN")
        if label == "CLEAN":
            continue
        if (tool, pos) in REGISTERED_SANITIZERS:
            continue  # discharged by a position-specific sanitizer
        raise TaintViolation(
            f"tainted value {label!r} reached sink {tool}.{pos} "
            f"without a registered sanitizer"
        )
    # Step 4: admit.
