diff --git a/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py b/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py index 0d45b5ff238..34c4d67a829 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py +++ b/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py @@ -304,11 +304,17 @@ class Agent365Guardrail(CustomGuardrail): reason="the Agent 365 endpoint returned a non-object JSON body", ) verdict: Final[_EvaluateResponse] = parsed_verdict + allowed: Final = verdict.get("allowed") + if not isinstance(allowed, bool): + return self._handle_unavailable( + data=data, + tool_name=tool_name, + reason="the Agent 365 endpoint returned a verdict without a boolean 'allowed' field", + ) raw_defender: Final = verdict.get("defender") defender: Final = raw_defender if isinstance(raw_defender, dict) else _DefenderResult() raw_correlation_id: Final = verdict.get("correlationId") correlation_id: Final = raw_correlation_id if isinstance(raw_correlation_id, str) else None - allowed: Final = verdict.get("allowed") is True self._record_verdict( data=data, verdict="Allow" if allowed else "Block", diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_agent_365.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_agent_365.py index e9cdf9318a6..2f037418927 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_agent_365.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_agent_365.py @@ -592,6 +592,37 @@ class TestMalformedResponses: await _run(guardrail, _mcp_data()) assert exc_info.value.status_code == 503 + @pytest.mark.asyncio + @pytest.mark.parametrize( + "verdict", + [{}, {"allowed": None}, {"allowed": "true"}, {"allowed": 1}, {"allowed": "false"}], + ids=["missing", "null", "string-true", "int-one", "string-false"], + ) + async def test_evaluate_non_boolean_allowed_fail_closed(self, verdict: dict): + handler: Final = FakeHandler([_token_response(), _response(200, verdict)]) + guardrail: Final = _make_guardrail(handler) + data: Final = _mcp_data() + with pytest.raises(HTTPException) as exc_info: + await _run(guardrail, data) + assert exc_info.value.status_code == 503 + assert "boolean 'allowed'" in exc_info.value.detail["message"] + assert _guardrail_info(data)["guardrail_response"]["verdict"] == "Unavailable" + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "verdict", + [{}, {"allowed": None}, {"allowed": "true"}, {"allowed": 1}, {"allowed": "false"}], + ids=["missing", "null", "string-true", "int-one", "string-false"], + ) + async def test_evaluate_non_boolean_allowed_fail_open(self, verdict: dict): + handler: Final = FakeHandler([_token_response(), _response(200, verdict)]) + guardrail: Final = _make_guardrail(handler, unreachable_fallback="fail_open") + data: Final = _mcp_data() + result: Final = await _run(guardrail, data) + assert result is data + assert _guardrail_info(data)["guardrail_response"]["verdict"] == "Unscanned" + assert _guardrail_info(data)["guardrail_status"] == "guardrail_failed_to_respond" + @pytest.mark.asyncio async def test_bad_expires_in_still_allows(self): handler: Final = FakeHandler(