From 96d79c913580f09a73ca9060344f727c4891b186 Mon Sep 17 00:00:00 2001 From: yucheng Date: Fri, 25 Sep 2026 19:50:02 +0000 Subject: [PATCH] fix(guardrails): default a scheme-less Agent 365 authority host to https and treat a null fallback as unset in the YAML preview Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../guardrail_hooks/agent_365/agent_365.py | 3 ++- .../guardrails/guardrail_hooks/test_agent_365.py | 15 +++++++++++++++ .../guardrails/_components/TeamGuardrailsTab.tsx | 2 +- .../_components/teamGuardrailConfigYaml.test.ts | 9 +++++++++ .../_components/teamGuardrailConfigYaml.ts | 4 ++-- 5 files changed, 29 insertions(+), 4 deletions(-) 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 a25c4ba5cbd..1b160b16c13 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py +++ b/litellm/proxy/guardrails/guardrail_hooks/agent_365/agent_365.py @@ -178,7 +178,8 @@ class Agent365Guardrail(CustomGuardrail): self.api_base = api_base.rstrip("/") self.resource_app_id = resource_app_id self.agent_id = agent_id - self.authority_host = authority_host.rstrip("/") + authority: Final = authority_host.strip().rstrip("/") + self.authority_host = authority if "://" in authority else f"https://{authority}" self.request_timeout = request_timeout self.unreachable_fallback: Literal["fail_closed", "fail_open"] = ( "fail_closed" if unreachable_fallback == "fail_closed" else "fail_open" 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 78871adbeda..1ba0064dcf5 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 @@ -307,6 +307,14 @@ class TestInitializeGuardrail: ) assert explicit.authority_host == "http://127.0.0.1:9" + def test_scheme_less_authority_host_env_gets_https(self, monkeypatch): + monkeypatch.setenv("AZURE_AUTHORITY_HOST", " login.microsoftonline.us/ ") + guardrail: Final = initialize_guardrail( + LitellmParams(guardrail="agent_365", mode="pre_mcp_call", tenant_id="t", client_id="c", client_secret="s"), + {"guardrail_name": "a365-bare-host"}, + ) + assert guardrail.authority_host == "https://login.microsoftonline.us" + def test_explicit_params_win(self, monkeypatch): monkeypatch.setenv("AGENT365_TENANT_ID", "env-tenant") params: Final = LitellmParams( @@ -381,6 +389,13 @@ class TestAllowFlow: assert handler.calls[0].url == "https://login.microsoftonline.us/tenant-abc/oauth2/v2.0/token" assert handler.calls[1].url == EVALUATE_URL + @pytest.mark.asyncio + async def test_obo_exchange_adds_https_to_a_bare_authority_host(self): + handler: Final = FakeHandler([_token_response(), _allow_response()]) + guardrail: Final = _make_guardrail(handler, authority_host="login.microsoftonline.us") + await _run(guardrail, _mcp_data()) + assert handler.calls[0].url == "https://login.microsoftonline.us/tenant-abc/oauth2/v2.0/token" + @pytest.mark.asyncio async def test_evaluate_payload(self): handler: Final = FakeHandler([_token_response(), _allow_response()]) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx index 5841d0162a5..b6cd98d8152 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/TeamGuardrailsTab.tsx @@ -150,7 +150,7 @@ function submissionToTeamGuardrail(item: GuardrailSubmissionItem): TeamGuardrail submittedAt: formatSubmissionDate(item.submitted_at), submittedBy: item.submitted_by_email ?? item.submitted_by_user_id ?? "—", mode: params.mode as string | undefined, - unreachable_fallback: params.unreachable_fallback as string | undefined, + unreachable_fallback: params.unreachable_fallback as string | null | undefined, additionalProviderParams: params.additional_provider_specific_params as Record | undefined, guardrailType: params.guardrail as string | undefined, }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.test.ts index e521dd49e3f..96987325419 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.test.ts @@ -59,6 +59,15 @@ describe("buildEquivalentConfigYaml unreachable_fallback line", () => { expect(untyped).toContain(" guardrail: generic_guardrail_api"); }); + it("treats a null fallback from the API as unset and shows the per-guardrail default", () => { + expect( + fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "agent_365", unreachable_fallback: null }))), + ).toBe("unreachable_fallback: fail_open"); + expect( + fallbackLine(buildEquivalentConfigYaml(guardrail({ guardrailType: "akto", unreachable_fallback: null }))), + ).toBe("unreachable_fallback: fail_closed"); + }); + it("keeps an explicit override over the per-guardrail default", () => { expect( fallbackLine( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.ts b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.ts index 824bf37b8f2..c4ad6b61e8c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/teamGuardrailConfigYaml.ts @@ -18,7 +18,7 @@ export type TeamGuardrail = { submittedAt: string; submittedBy: string; mode?: string; - unreachable_fallback?: string; + unreachable_fallback?: string | null; additionalProviderParams?: Record; guardrailType?: string; }; @@ -33,7 +33,7 @@ export function defaultUnreachableFallback(guardrailType: string | undefined): " function unreachableFallbackLine(g: TeamGuardrail): string { const hint = "fail_closed blocks, fail_open proceeds when the guardrail endpoint is unreachable"; - if (g.unreachable_fallback !== undefined) { + if (g.unreachable_fallback) { return ` unreachable_fallback: ${g.unreachable_fallback} # ${hint}`; } return ` unreachable_fallback: ${defaultUnreachableFallback(g.guardrailType)} # ${hint}. Shown value is this guardrail's default.`;