# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-refuse-to-load-a-signed-skill-manifest-the-signature-scope-divergence-boundary-in-dynamic-agent-skill-registries.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-refuse-to-load-a-signed-skill-manifest-the-signature-scope-divergence-boundary-in-dynamic-agent-skill-registries.warrant.json
# Markdown SHA-256:   803d300ddd3ec0e3f6fa15f2867ade97cec71db111e0f33faf7118d8a4cd835d
# Code AST SHA-256:   ca8e8796ed91a938ec042ff71ab70c89183f5b30c9f648accf445141080a2c4b
# Verified Classes:   ScopeBindingError, SignedManifest, LoadContext
# Defensive Guards:   6 fail-closed invariants (zero external deps)

import hashlib
from dataclasses import dataclass
from typing import FrozenSet, Mapping


class ScopeBindingError(RuntimeError):
    """Raised when a signed manifest is valid but not authorized for the target scope."""


@dataclass(frozen=True)
class SignedManifest:
    manifest_digest: str
    declared_scope: str
    provides: Mapping[str, str]          # tool_name -> entrypoint_digest
    dependencies: Mapping[str, str]      # dep_name -> resolved_digest (must be pinned)
    signer_key_id: str
    signature: bytes
    signed_binding: FrozenSet[str]       # tuple fields actually covered by signature


@dataclass(frozen=True)
class LoadContext:
    target_scope: str
    registry_epoch: int
    key_authorization: Mapping[str, FrozenSet[str]]  # key_id -> authorized scopes


REQUIRED_BINDING_FIELDS = frozenset(
    {"manifest_digest", "scope", "registry_epoch", "resolved_dependency_digests"}
)


def verify_and_bind(manifest: SignedManifest, ctx: LoadContext, verifier) -> None:
    # 1. Cryptographic validity of the body.
    if not verifier(manifest.signer_key_id, manifest.manifest_digest, manifest.signature):
        raise ScopeBindingError(
            f"signature invalid for manifest {manifest.manifest_digest}"
        )

    # 2. The signature must cover the *binding*, not just the body.
    missing = REQUIRED_BINDING_FIELDS - manifest.signed_binding
    if missing:
        raise ScopeBindingError(
            f"manifest {manifest.manifest_digest} signature does not cover binding "
            f"fields {sorted(missing)}; refusing to bind into scope {ctx.target_scope}"
        )

    # 3. Scope authorization: a valid key is not authority over this scope.
    authorized = ctx.key_authorization.get(manifest.signer_key_id, frozenset())
    if ctx.target_scope not in authorized:
        raise ScopeBindingError(
            f"signer {manifest.signer_key_id} not authorized for scope "
            f"{ctx.target_scope} (authorized: {sorted(authorized)})"
        )

    # 4. Declared scope must match the binding target.
    if manifest.declared_scope != ctx.target_scope:
        raise ScopeBindingError(
            f"manifest declares scope {manifest.declared_scope!r} but is being "
            f"bound into {ctx.target_scope!r}"
        )

    # 5. Dependencies must be pinned, not wildcarded.
    for dep, digest in manifest.dependencies.items():
        if not digest or digest == "*":
            raise ScopeBindingError(
                f"dependency {dep!r} is unpinned in signed manifest "
                f"{manifest.manifest_digest}; refusing (dependency-swap surface)"
            )

    # 6. Every provides entry must resolve within the declared scope.
    for tool_name, entry in manifest.provides.items():
        if not tool_name.startswith(f"{ctx.target_scope}."):
            raise ScopeBindingError(
                f"tool {tool_name!r} provides into namespace outside scope "
                f"{ctx.target_scope!r}; scope field is not advisory"
            )


def load_skill(manifest: SignedManifest, ctx: LoadContext, verifier) -> str:
    verify_and_bind(manifest, ctx, verifier)
    return hashlib.sha256(
        f"{manifest.manifest_digest}:{ctx.target_scope}:{ctx.registry_epoch}".encode()
    ).hexdigest()
