diff --git a/nanobot/agent/tools/base.py b/nanobot/agent/tools/base.py index 46cc4b472..02445b15f 100644 --- a/nanobot/agent/tools/base.py +++ b/nanobot/agent/tools/base.py @@ -1,6 +1,7 @@ """Base class for agent tools.""" from __future__ import annotations +import math import typing from abc import ABC, abstractmethod from collections.abc import Callable @@ -67,6 +68,8 @@ class Schema(ABC): return [f"{label} should be number"] if t in _JSON_TYPE_MAP and t not in ("integer", "number") and not isinstance(val, _JSON_TYPE_MAP[t]): return [f"{label} should be {t}"] + if t == "number" and isinstance(val, float) and not math.isfinite(val): + return [f"{label} must be finite"] errors: list[str] = [] if "enum" in schema and val not in schema["enum"]: diff --git a/tests/tools/test_tool_validation.py b/tests/tools/test_tool_validation.py index fdb881f45..f461ccf3b 100644 --- a/tests/tools/test_tool_validation.py +++ b/tests/tools/test_tool_validation.py @@ -598,6 +598,24 @@ def test_cast_params_invalid_string_to_number() -> None: assert result["rate"] == "not_a_number" +@pytest.mark.parametrize( + "value", + [float("nan"), float("inf"), float("-inf"), "NaN", "Infinity", "-Infinity"], +) +def test_cast_params_rejects_non_finite_numbers(value: float | str) -> None: + """JSON number parameters must remain finite after schema-driven casting.""" + tool = CastTestTool( + { + "type": "object", + "properties": {"rate": {"type": "number"}}, + } + ) + + result = tool.cast_params({"rate": value}) + + assert tool.validate_params(result) == ["rate must be finite"] + + def test_validate_params_bool_not_accepted_as_number() -> None: """Booleans should not pass number validation.""" tool = CastTestTool(