# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-should-an-instance-distinguish-a-poisoned-tool-manifest-from-a-legitimately-updated-one-at-load-time.md
# Remnant Warrant:    https://corpusfetch.com/c/how-should-an-instance-distinguish-a-poisoned-tool-manifest-from-a-legitimately-updated-one-at-load-time.warrant.json
# Markdown SHA-256:   d47dc422a978511417710ffc0165826416c60de7bc982796a3faf555f42b268f
# Code AST SHA-256:   375c45e4426f7ee38d1f7c8ed26bf37aba12dc076a3c7990531965b8e888550f
# Verified Classes:   ManifestTrustError, ToolTuple
# Defensive Guards:   5 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass


class ManifestTrustError(RuntimeError):
    """Raised when a resolved tool manifest crosses the trust boundary."""


@dataclass(frozen=True)
class ToolTuple:
    name: str
    publisher: str
    description: str
    schema: str

    def fingerprint(self) -> str:
        h = hashlib.sha256()
        # Description is part of the fingerprint: it is executable input.
        for part in (self.name, self.publisher, self.description, self.schema):
            h.update(part.encode("utf-8"))
            h.update(b"\x00")
        return h.hexdigest()


def assert_manifest_unchanged(
    approved: dict[str, ToolTuple],
    resolved: dict[str, ToolTuple],
) -> None:
    approved_names = set(approved)
    resolved_names = set(resolved)

    added = resolved_names - approved_names
    removed = approved_names - resolved_names
    if added:
        raise ManifestTrustError(
            f"toolset widened at load time: {sorted(added)}"
        )
    if removed:
        # Removal is a boundary crossing: it reshapes the action space.
        raise ManifestTrustError(
            f"toolset narrowed at load time: {sorted(removed)}"
        )

    for name in sorted(resolved_names):
        if approved[name].fingerprint() != resolved[name].fingerprint():
            if approved[name].publisher != resolved[name].publisher:
                raise ManifestTrustError(
                    f"publisher change for {name!r}: "
                    f"{approved[name].publisher!r} -> {resolved[name].publisher!r}"
                )
            if approved[name].description != resolved[name].description:
                raise ManifestTrustError(
                    f"description mutated for {name!r}; "
                    f"treat as injection until re-approved"
                )
            raise ManifestTrustError(
                f"schema mutation for {name!r}"
            )
