# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-poisoned-tool-description-versus-a-legitimate-mcp-server-upgrade.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-poisoned-tool-description-versus-a-legitimate-mcp-server-upgrade.warrant.json
# Markdown SHA-256:   f408c1f373cf79a12fcc9c6192e959c07cd02fed08e9ef079f60258b605f397d
# Code AST SHA-256:   33c94710f4a283fc989b1d5d4f749b2b5a00a12c976c4a252c07e91552b966fb
# Verified Classes:   PoisonedToolDescriptionError, ToolBinding
# Defensive Guards:   5 fail-closed invariants (zero external deps)

import hashlib
import re
from dataclasses import dataclass


class PoisonedToolDescriptionError(RuntimeError):
    """Raised when a tool description fails the structural binding check."""
    pass


# Instruction verbs that govern model behavior, not tool capability.
_INSTRUCTION_VERBS = re.compile(
    r"\b(ignore|override|instead|before responding|always|never|"
    r"do not|must call|prefer this tool|use this tool)\b",
    re.IGNORECASE,
)
_SECOND_PERSON = re.compile(r"\byou\b|\byour\b", re.IGNORECASE)
_ZERO_WIDTH = re.compile(r"[\u200b-\u200f\u202a-\u202e\ufeff]")
_SCOPE_ESCALATION_KEYS = {"path", "url", "env", "command", "cmd", "shell"}


@dataclass(frozen=True)
class ToolBinding:
    server_id: str
    tool_name: str
    version: str
    description: str
    schema_properties: frozenset

    def description_hash(self) -> str:
        return hashlib.sha256(self.description.encode("utf-8")).hexdigest()


def assert_bindable(
    candidate: ToolBinding,
    pinned_hashes: dict[tuple[str, str, str], str],
    baseline_properties: frozenset | None = None,
) -> None:
    key = (candidate.server_id, candidate.tool_name, candidate.version)
    pinned = pinned_hashes.get(key)

    if pinned is None:
        raise PoisonedToolDescriptionError(
            f"no pinned baseline for {key}; first load requires human review"
        )

    if candidate.description_hash() == pinned:
        return  # unchanged description: bind

    # Description changed under a pinned version — inspect the delta.
    if _ZERO_WIDTH.search(candidate.description):
        raise PoisonedToolDescriptionError(
            f"zero-width characters in description for {key}"
        )
    if _SECOND_PERSON.search(candidate.description):
        raise PoisonedToolDescriptionError(
            f"second-person addressing of the model in {key}"
        )
    if _INSTRUCTION_VERBS.search(candidate.description):
        raise PoisonedToolDescriptionError(
            f"instruction verbs in capability field for {key}"
        )
    if baseline_properties is not None:
        escalated = candidate.schema_properties - baseline_properties
        if escalated & _SCOPE_ESCALATION_KEYS:
            raise PoisonedToolDescriptionError(
                f"schema scope escalation {sorted(escalated)} for {key} "
                f"without capability claim"
            )
    # Pure capability change with clean description: permitted.
    return


def load_toolset(
    candidates: list[ToolBinding],
    pinned_hashes: dict[tuple[str, str, str], str],
    baselines: dict[tuple[str, str, str], frozenset],
) -> list[ToolBinding]:
    bound: list[ToolBinding] = []
    for c in candidates:
        key = (c.server_id, c.tool_name, c.version)
        assert_bindable(c, pinned_hashes, baselines.get(key))
        bound.append(c)
    return bound
