"""Consumer-side validation for backend-exposed tool availability. This module keeps Person 2's prompt and policy layers fail-closed. The mock and real backends may evolve independently during integration, so we validate the adapter contract before building prompts or selecting actions. """ from __future__ import annotations from .tool_catalog import TOOL_SPEC_BY_NAME class ToolAvailabilityError(ValueError): """Raised when the backend omits or corrupts the available-tools contract.""" def validate_tool_availability(available_tools: list[str] | None) -> list[str]: """Validate adapter-exposed tools and return a normalized list. The consumer stack must fail closed here: a missing or empty tool list is a contract violation during integration, not a signal to expose the full catalog speculatively. """ if available_tools is None: raise ToolAvailabilityError( "available_tools is missing from the backend contract; refusing to build prompts or select actions." ) if not isinstance(available_tools, list): raise ToolAvailabilityError( f"available_tools must be a list of tool names, received {type(available_tools).__name__}." ) normalized = [tool_name for tool_name in available_tools if isinstance(tool_name, str) and tool_name.strip()] if not normalized: raise ToolAvailabilityError( "available_tools is empty; the backend must expose at least one supported tool." ) unknown_tools = sorted(set(normalized) - set(TOOL_SPEC_BY_NAME)) if unknown_tools: raise ToolAvailabilityError( f"available_tools contains unknown tool names: {', '.join(unknown_tools)}." ) return normalized