File size: 1,759 Bytes
9b1756a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
"""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