# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-structural-signals-indicate-a-poisoned-mcp-tool-manifest-versus-a-merely-over-privileged-one.md
# Remnant Warrant:    https://corpusfetch.com/c/what-structural-signals-indicate-a-poisoned-mcp-tool-manifest-versus-a-merely-over-privileged-one.warrant.json
# Markdown SHA-256:   c377de13828f5a1a1e293cd486605efdb81c6dd754ea60e49d02b8db1735484f
# Code AST SHA-256:   23ed2d7056a8a0008e1028696064d1b9a1886b865c8dbbe70ae2567c1404ac77
# Verified Classes:   ManifestIntegrityError, ToolManifest
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import Any


class ManifestIntegrityError(RuntimeError):
    """Raised when a tool manifest is structurally inconsistent (poisoned)."""


@dataclass
class ToolManifest:
    name: str
    description: str
    input_schema: dict[str, Any]
    read_only_hint: bool
    required: list[str]


WRITE_SHAPED = {"path", "destination", "command", "url", "endpoint", "target"}


def audit_manifest(
    manifest: ToolManifest,
    registered_names: dict[str, str],
) -> None:
    declared = set(manifest.input_schema.get("properties", {}).keys())
    permissive = manifest.input_schema.get("additionalProperties", True)

    # Signal 1: description references an undeclared argument.
    for token in _argument_tokens(manifest.description):
        if token not in declared and token not in _STOPWORDS:
            if not permissive:
                raise ManifestIntegrityError(
                    f"{manifest.name}: description references undeclared "
                    f"argument {token!r} while additionalProperties=false "
                    f"(schema/description divergence)"
                )
            raise ManifestIntegrityError(
                f"{manifest.name}: description smuggles undeclared argument "
                f"{token!r} into a permissive schema (side-channel route)"
            )

    # Signal 2: read-only annotation contradicted by write-shaped required args.
    if manifest.read_only_hint:
        write_args = WRITE_SHAPED.intersection(manifest.required)
        if write_args:
            raise ManifestIntegrityError(
                f"{manifest.name}: readOnlyHint=true but requires write-shaped "
                f"arguments {sorted(write_args)} (annotation/behavior contradiction)"
            )

    # Signal 3: namespace shadowing with divergent semantics.
    if manifest.name in registered_names:
        prior = registered_names[manifest.name]
        if _semantic_delta(prior, manifest.description) > 0:
            raise ManifestIntegrityError(
                f"{manifest.name}: shadows a registered tool with divergent "
                f"description (namespace shadowing)"
            )


def _argument_tokens(description: str) -> set[str]:
    # Extract backtick-quoted identifiers as candidate argument references.
    import re
    return set(re.findall(r"`([a-zA-Z_][a-zA-Z0-9_]*)`", description))


def _semantic_delta(a: str, b: str) -> int:
    return 1 if a.strip() != b.strip() else 0


_STOPWORDS = {"context", "value", "input"}
