# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-does-an-instance-detect-a-skill-injection-attack-when-the-injected-capability-is-indistinguishable-from-a-legitimate-one-at-load-time.md
# Remnant Warrant:    https://corpusfetch.com/c/how-does-an-instance-detect-a-skill-injection-attack-when-the-injected-capability-is-indistinguishable-from-a-legitimate-one-at-load-time.warrant.json
# Markdown SHA-256:   ecab9c46745b6cb4a39d69ce8ebcae39e2b3b2f7c488a618445570b8061a12ad
# Code AST SHA-256:   50cf84d1679e4c6611af79ae36602b345b58889981dcc679035673e700093f00
# Verified Classes:   SkillInjectionError, ArgumentDomain, SkillDeclaration, ObservedCall
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass, field
from typing import FrozenSet


class SkillInjectionError(RuntimeError):
    """Raised when an invoked skill's argument-domain footprint exceeds its declaration."""
    pass


@dataclass(frozen=True)
class ArgumentDomain:
    tool: str
    path_prefix: str | None = None
    host_allowlist: FrozenSet[str] = frozenset()


@dataclass(frozen=True)
class SkillDeclaration:
    skill_id: str
    declared_domains: FrozenSet[ArgumentDomain]
    tolerance_prefixes: FrozenSet[str] = frozenset()


@dataclass(frozen=True)
class ObservedCall:
    tool: str
    path: str | None = None
    host: str | None = None


def check_binding(decl: SkillDeclaration, trace: list[ObservedCall]) -> None:
    for call in trace:
        covered = False
        for dom in decl.declared_domains:
            if dom.tool != call.tool:
                continue
            if call.path is not None and dom.path_prefix is not None:
                if call.path.startswith(dom.path_prefix):
                    covered = True
                    break
            if call.host is not None and call.host in dom.host_allowlist:
                covered = True
                break
            if call.path is None and call.host is None:
                covered = True
                break
        if not covered:
            in_tolerance = call.path is not None and any(
                call.path.startswith(p) for p in decl.tolerance_prefixes
            )
            if in_tolerance:
                continue
            raise SkillInjectionError(
                f"skill {decl.skill_id!r} invoked {call.tool!r} outside declared "
                f"argument domain (path={call.path!r}, host={call.host!r}); "
                f"halting binding and quarantining skill for this task"
            )
