# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/when-must-a-skill-registry-reject-a-dynamically-loaded-skill-before-granting-it-tool-authority.md
# Remnant Warrant:    https://corpusfetch.com/c/when-must-a-skill-registry-reject-a-dynamically-loaded-skill-before-granting-it-tool-authority.warrant.json
# Markdown SHA-256:   a8cd9f2a0269ed1d19445cafafc8d76af1ef2b06db165f0eaa75f2a332cb9e69
# Code AST SHA-256:   6388a5ca515c559babdef5bef3afb3ebcc6234b71352da2a8ed1d2644fe499b6
# Verified Classes:   SkillAuthorityError, SkillArtifact, Grant
# Defensive Guards:   4 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass, field


class SkillAuthorityError(RuntimeError):
    """Raised when a dynamically loaded skill cannot be proven to match its grant."""
    pass


@dataclass
class SkillArtifact:
    name: str
    version: str
    capabilities: frozenset
    payload: bytes

    def artifact_hash(self) -> str:
        return hashlib.sha256(self.payload).hexdigest()


@dataclass
class Grant:
    artifact_hash: str
    capabilities: frozenset
    validated_at_load: bool = True


RESERVED_NAMESPACES = {"fetch_url", "exec_shell", "read_file"}


def grant_authority(artifact: SkillArtifact, prior: Grant | None) -> Grant:
    # Reject shadowing of reserved names.
    if artifact.name in RESERVED_NAMESPACES:
        raise SkillAuthorityError(
            f"skill '{artifact.name}' attempts to bind a reserved namespace"
        )

    current_hash = artifact.artifact_hash()

    # If there is a prior grant, the artifact must be byte-identical AND the
    # capability set must be unchanged. Additive changes are NOT safe.
    if prior is not None:
        if prior.artifact_hash != current_hash:
            raise SkillAuthorityError(
                f"artifact bytes changed since validation for '{artifact.name}'; "
                "re-validation required before grant"
            )
        if prior.capabilities != artifact.capabilities:
            raise SkillAuthorityError(
                f"capability set for '{artifact.name}' changed "
                f"{sorted(prior.capabilities)} -> {sorted(artifact.capabilities)}; "
                "additive capability changes require re-authorization"
            )
        return prior

    # Fresh grant: authority is computed from the validated bytes only.
    return Grant(artifact_hash=current_hash, capabilities=artifact.capabilities)


def bind_and_invoke(artifact: SkillArtifact, prior: Grant | None) -> Grant:
    grant = grant_authority(artifact, prior)
    # Invariant check: the bytes about to execute must match the grant.
    if artifact.artifact_hash() != grant.artifact_hash:
        raise SkillAuthorityError(
            f"check/use mismatch for '{artifact.name}': granted hash "
            f"{grant.artifact_hash} != executing hash {artifact.artifact_hash()}"
        )
    return grant
