# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-happens-when-an-instance-trusts-a-tool-manifest-s-declared-side-effect-class-during-hot-reload.md
# Remnant Warrant:    https://corpusfetch.com/c/what-happens-when-an-instance-trusts-a-tool-manifest-s-declared-side-effect-class-during-hot-reload.warrant.json
# Markdown SHA-256:   25d20896a5c53e689a89719b0f411009e482b5addb78835be3abe3b2407095ad
# Code AST SHA-256:   927654f0a2e9b55c46bb27732b04696673e0004b334286be1c12692a790464b0
# Verified Classes:   SideEffectClass, ManifestTrustError, ToolManifest
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from __future__ import annotations

import hashlib
from dataclasses import dataclass
from enum import Enum


class SideEffectClass(str, Enum):
    READ_ONLY = "read_only"
    MUTATING = "mutating"


class ManifestTrustError(RuntimeError):
    """Raised when a dynamically loaded tool manifest cannot be soundly admitted."""


@dataclass(frozen=True)
class ToolManifest:
    name: str
    payload_hash: str
    declared_class: SideEffectClass
    description: str
    signed: bool
    pinned_hash: str | None  # hash from an out-of-band trust policy, if any


def admit_manifest(
    manifest: ToolManifest,
    *,
    description_passthrough: bool,
) -> SideEffectClass:
    """
    Return the *effective* side-effect class the runtime may honor.

    The declared class is honored only when the manifest is both signed and
    pinned, AND the description field is not fed to the planner as instruction.
    Otherwise the class is downgraded to the most restrictive value.
    """
    if description_passthrough:
        # The description is an instruction channel into the planner; the
        # manifest cannot be trusted for class purposes even if signed.
        raise ManifestTrustError(
            f"manifest {manifest.name!r}: description passthrough is enabled; "
            "declared side-effect class cannot be honored"
        )

    if not manifest.signed:
        return SideEffectClass.MUTATING

    if manifest.pinned_hash is None:
        return SideEffectClass.MUTATING

    if manifest.pinned_hash != manifest.payload_hash:
        raise ManifestTrustError(
            f"manifest {manifest.name!r}: payload hash {manifest.payload_hash!r} "
            f"does not match pinned hash {manifest.pinned_hash!r} "
            "(possible mid-session swap)"
        )

    # Signed, pinned, no description passthrough: honor the declaration.
    return manifest.declared_class


def re_admit_on_list_changed(
    cached: ToolManifest,
    incoming: ToolManifest,
) -> SideEffectClass:
    """
    Handle a tools/list_changed event. A same-name manifest with a different
    payload hash is a *new* tool and must be re-admitted from scratch.
    """
    if incoming.name != cached.name:
        raise ManifestTrustError(
            f"list_changed name mismatch: cached={cached.name!r} incoming={incoming.name!r}"
        )

    if incoming.payload_hash != cached.payload_hash:
        # Do not carry forward the cached admission. Re-run full admission.
        return admit_manifest(incoming, description_passthrough=False)

    return admit_manifest(incoming, description_passthrough=False)


def hash_payload(payload: bytes) -> str:
    return hashlib.sha256(payload).hexdigest()
