# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/which-invariants-prevent-consent-scope-drift-in-mcp-authorization-extensions-built-on-oauth-2-1-resource-server-patterns.md
# Remnant Warrant:    https://corpusfetch.com/c/which-invariants-prevent-consent-scope-drift-in-mcp-authorization-extensions-built-on-oauth-2-1-resource-server-patterns.warrant.json
# Markdown SHA-256:   228171f0c54ae38cda4f8a482336713a45a3d0c47418534a50e837da88670e1e
# Code AST SHA-256:   b7b297d16d6f6688d2ad294fd3e50fc9d14989a887c93b58785c594044918b43
# Verified Classes:   ConsentScopeDriftError, InvocationContext
# Defensive Guards:   4 fail-closed invariants (zero external deps)

from dataclasses import dataclass
from typing import FrozenSet


class ConsentScopeDriftError(RuntimeError):
    """Raised when effective tool authority escapes the consent/token subset chain."""


@dataclass(frozen=True)
class InvocationContext:
    requested_scopes: FrozenSet[str]
    consent_scopes: FrozenSet[str]      # persisted, bound via consent_id claim
    token_scopes: FrozenSet[str]        # scope claim from validated access token
    consent_id_in_token: str
    consent_id_in_store: str


def authorize_tool_call(ctx: InvocationContext) -> FrozenSet[str]:
    # Rule 1: consent binding must match; a valid token cannot widen consent.
    if ctx.consent_id_in_token != ctx.consent_id_in_store:
        raise ConsentScopeDriftError(
            f"consent binding mismatch: token={ctx.consent_id_in_token!r} "
            f"store={ctx.consent_id_in_store!r}"
        )

    # Rule 2: token scope is a ceiling, not a floor.
    if not ctx.consent_scopes <= ctx.token_scopes:
        raise ConsentScopeDriftError(
            f"consent exceeds current token authority: "
            f"consent-token={sorted(ctx.consent_scopes - ctx.token_scopes)}"
        )

    # Rule 3: requested must be within BOTH consent and token.
    if not ctx.requested_scopes <= ctx.consent_scopes:
        raise ConsentScopeDriftError(
            f"requested outside consent: "
            f"{sorted(ctx.requested_scopes - ctx.consent_scopes)}"
        )
    if not ctx.requested_scopes <= ctx.token_scopes:
        raise ConsentScopeDriftError(
            f"requested outside token: "
            f"{sorted(ctx.requested_scopes - ctx.token_scopes)}"
        )

    return ctx.requested_scopes  # effective = requested, now proven within chain
