From fee2ff525dd0194ddb9ed7acf5e24520f8484323 Mon Sep 17 00:00:00 2001 From: albertbausili Date: Sun, 13 Sep 2026 13:40:17 +0200 Subject: [PATCH] fix(guardrails): land the TrustGuard ask verdict as a block TrustGuard reduces findings to block, ask, transform, report, or allow. The hook only knew four of them, so a policy with an Ask gate made every evaluation on that collector fail closed with 503 "unknown verdict", which reads as an outage rather than a policy decision A proxy has no approval flow to hand the question to, so ask now raises the same 400 as block. The response carries the verdict so operators can tell the two apart in the error body and in Activity --- .../proxy/guardrails/guardrail_hooks/neuraltrust/README.md | 1 + .../guardrails/guardrail_hooks/neuraltrust/neuraltrust.py | 7 +++++-- .../proxy/guardrails/guardrail_hooks/test_neuraltrust.py | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md index 2563eef227e..d6ba8f635bb 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md +++ b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/README.md @@ -34,6 +34,7 @@ Each evaluate call carries `session_id` from the LiteLLM session and `consumer_i | TrustGuard `status` | LiteLLM | | --- | --- | | `block` | HTTP 400 (trace_id / request_id only; findings are not echoed) | +| `ask` | HTTP 400 like `block`: a proxy has no approval flow, so the response names `verdict: ask` | | `transform` | rewrite the last user message / last text from `transformed_payload` | | `report` / `allow` | pass through (`report` is logged by trace_id) | diff --git a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py index d20cc3ce37c..1106f856c5b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py +++ b/litellm/proxy/guardrails/guardrail_hooks/neuraltrust/neuraltrust.py @@ -43,10 +43,12 @@ CONSUMER_ID_KEYS: Final = ( METADATA_ADAPTER: Final = TypeAdapter(Mapping[str, object]) EMPTY_METADATA: Final[Mapping[str, object]] = MappingProxyType({}) STATUS_BLOCK: Final = "block" +STATUS_ASK: Final = "ask" STATUS_TRANSFORM: Final = "transform" STATUS_REPORT: Final = "report" STATUS_ALLOW: Final = "allow" -KNOWN_STATUSES: Final = frozenset({STATUS_ALLOW, STATUS_BLOCK, STATUS_TRANSFORM, STATUS_REPORT}) +BLOCKING_STATUSES: Final = frozenset({STATUS_BLOCK, STATUS_ASK}) +KNOWN_STATUSES: Final = frozenset({STATUS_ALLOW, STATUS_TRANSFORM, STATUS_REPORT, *BLOCKING_STATUSES}) UNREACHABLE_HTTP_STATUSES: Final = frozenset({502, 504}) TRANSFORM_MISSING: Final = "TrustGuard transform missing payload" @@ -245,12 +247,13 @@ class NeuralTrustGuardrail(CustomGuardrail): return self._handle_unreachable(inputs, exc) status: Final = result["status"] - if status == STATUS_BLOCK: + if status in BLOCKING_STATUSES: raise HTTPException( status_code=400, detail={ # mutable-ok: FastAPI HTTPException.detail is a JSON object "error": "Violated guardrail policy", "neuraltrust_guardrail_response": "Blocked by NeuralTrust TrustGuard.", + "verdict": status, "trace_id": result.get("trace_id"), "request_id": result.get("request_id"), }, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py index 458987a8cd1..3476243bdb5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_neuraltrust.py @@ -231,12 +231,13 @@ class TestNeuralTrustGuardrail: assert "collector_key" not in mock_post.call_args.kwargs["json"] @pytest.mark.asyncio - async def test_block_raises_without_findings(self) -> None: + @pytest.mark.parametrize("status", ["block", "ask"]) + async def test_block_and_ask_raise_without_findings(self, status: str) -> None: guardrail = _guardrail() mock_post = AsyncMock( return_value=_response( { - "status": "block", + "status": status, "trace_id": "tr-1", "findings": [{"outcome": {"action": "block"}, "evidence": "ssn 123-45-6789"}], } @@ -256,6 +257,7 @@ class TestNeuralTrustGuardrail: assert "findings" not in detail assert "evidence" not in str(detail) assert detail["trace_id"] == "tr-1" + assert detail["verdict"] == status @pytest.mark.asyncio async def test_transform_rewrites_texts(self) -> None: