From 99b6a9bb8f878f151dab7d3f996a4abfe4e62cb6 Mon Sep 17 00:00:00 2001 From: Eneko Ruiz <196690101+eneekoruiz@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:05:24 +0200 Subject: [PATCH] fix: reject lossy classifier tool-call indices RED: 6 failures proved booleans and fractional indices could invent evidence. GREEN: 49 targeted tests; full skill-comply suites pass on Windows (88 passed, 5 platform skips) and Linux (93 passed). Parser coverage 100%; exact integer floats and numeric strings remain supported. Pi/catalog checks pass. (cherry picked from commit 6dde588860b440313c3cf63561f10203fcf28f55) --- skills/skill-comply/scripts/classifier.py | 2 ++ skills/skill-comply/tests/test_classifier.py | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/skills/skill-comply/scripts/classifier.py b/skills/skill-comply/scripts/classifier.py index 76d7c87aa..74361c8bf 100644 --- a/skills/skill-comply/scripts/classifier.py +++ b/skills/skill-comply/scripts/classifier.py @@ -85,6 +85,8 @@ def _parse_classification(text: str) -> dict[str, list[int]]: raise ClassifierParseError( f"classifier returned non-list JSON value: {type(value).__name__}" ) + if any(isinstance(i, bool) or (isinstance(i, float) and not i.is_integer()) for i in value): + raise ValueError("tool call indices must be whole numbers, not booleans") return {k: [int(i) for i in v] for k, v in parsed.items()} except (json.JSONDecodeError, ValueError, TypeError, OverflowError) as e: raise ClassifierParseError( diff --git a/skills/skill-comply/tests/test_classifier.py b/skills/skill-comply/tests/test_classifier.py index a94f44fe5..a9e80a599 100644 --- a/skills/skill-comply/tests/test_classifier.py +++ b/skills/skill-comply/tests/test_classifier.py @@ -28,7 +28,12 @@ class TestParseClassification: _parse_classification('{"a": [0], "other": "x"}') def test_existing_index_coercion_is_preserved(self) -> None: - assert _parse_classification('{"a": [true, 2.9, "3"]}') == {"a": [1, 2, 3]} + assert _parse_classification('{"a": [0, 2.0, "3"]}') == {"a": [0, 2, 3]} + + @pytest.mark.parametrize("index", [True, False, 2.9, -0.1]) + def test_boolean_and_fractional_indices_raise(self, index: object) -> None: + with pytest.raises(classifier.ClassifierParseError, match="failed to parse"): + _parse_classification(json.dumps({"a": [index]})) def test_empty_object_is_valid(self) -> None: assert _parse_classification("{}") == {} @@ -54,7 +59,7 @@ class TestParseClassification: assert issubclass(classifier.ClassifierParseError, RuntimeError) -@pytest.mark.parametrize("response", ["not JSON", "[]", '{"a": ["x"]}', '{"a": "x"}', '{"a": [0], "other": null}', '{"a": [1e309]}', '{"a": [-1e309]}']) +@pytest.mark.parametrize("response", ["not JSON", "[]", '{"a": ["x"]}', '{"a": "x"}', '{"a": [0], "other": null}', '{"a": [1e309]}', '{"a": [-1e309]}', '{"a": [false]}', '{"a": [0.5]}']) def test_grader_propagates_classifier_parse_failure(response: str) -> None: """An invalid successful subprocess response cannot become a zero score.""" spec = ComplianceSpec(