From a16073b2aa5cd75f61b3f68b35c234075d02680d Mon Sep 17 00:00:00 2001 From: yucheng Date: Sun, 27 Sep 2026 00:58:26 +0000 Subject: [PATCH] fix(guardrails): warn when agent 365 yaml still carries the removed override keys Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../guardrail_hooks/agent_365/__init__.py | 19 ++++++++++++++ .../guardrail_hooks/test_agent_365.py | 26 +++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py b/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py index b39a4f0e684..12ecb7f619f 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py +++ b/litellm/proxy/guardrails/guardrail_hooks/agent_365/__init__.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Final +from litellm._logging import verbose_proxy_logger from litellm.types.guardrails import SupportedGuardrailIntegrations from .agent_365 import Agent365Guardrail @@ -31,6 +32,24 @@ def initialize_guardrail(litellm_params: "LitellmParams", guardrail: "Guardrail" if not guardrail_name: raise ValueError("Microsoft Agent 365: guardrail_name is required") + extras: Final = litellm_params.model_extra or {} + ignored_overrides: Final = tuple( + key + for key, value in ( + ("api_base", litellm_params.api_base), + ("resource_app_id", extras.get("resource_app_id")), + ("agent_id", extras.get("agent_id")), + ) + if value is not None + ) + if ignored_overrides: + verbose_proxy_logger.warning( + "Microsoft Agent 365 (%s): ignoring %s; evaluations always go to the production Agent 365 endpoint " + "and the agent identity is the caller's key alias", + guardrail_name, + ", ".join(ignored_overrides), + ) + agent_365_guardrail: Final = Agent365Guardrail( guardrail_name=guardrail_name, tenant_id=tenant_id, 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 eed6143c4e7..99b45272754 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 @@ -244,8 +244,30 @@ class TestInitializeGuardrail: assert set(fields) == {"tenant_id", "client_id", "client_secret", "unreachable_fallback"} assert fields["unreachable_fallback"]["default_value"] == "fail_closed" - def test_config_model_has_no_endpoint_or_identity_overrides(self): - assert not {"api_base", "resource_app_id", "agent_id"} & set(Agent365GuardrailConfigModel.model_fields) + @pytest.mark.asyncio + async def test_stale_yaml_overrides_are_ignored_and_logged(self, caplog): + params: Final = LitellmParams( + guardrail="agent_365", + mode="pre_mcp_call", + tenant_id="tenant-abc", + client_id="client-xyz", + client_secret="secret-123", + default_on=True, + api_base="https://agent365.example.test", + resource_app_id="00000000-0000-0000-0000-000000000000", + agent_id="yaml-agent", + ) + with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"): + guardrail: Final = initialize_guardrail(params, {"guardrail_name": "a365-stale"}) + assert "ignoring api_base, resource_app_id, agent_id" in caplog.text + handler: Final = FakeHandler([_token_response(), _allow_response()]) + guardrail.async_handler = handler + await _run(guardrail, _mcp_data()) + token_call, evaluate_call = handler.calls + assert token_call.url == TOKEN_URL + assert token_call.data["scope"] == f"{AGENT_365_PROD_RESOURCE_APP_ID}/ThreatProtection.Evaluate.All" + assert evaluate_call.url == EVALUATE_URL + assert evaluate_call.json["agentId"] == "my-agent-key" def test_explicit_params_win(self, monkeypatch): monkeypatch.setenv("AGENT365_TENANT_ID", "env-tenant")