From 18d96358d62c116d0de7bcb05c48824c07616043 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:31:48 -0700 Subject: [PATCH] fix(responses): ask no team permission for string metadata named like a guardrail pick on socket turns --- .../proxy/response_api_endpoints/endpoints.py | 15 +++++++++++++-- .../response_api_endpoints/test_endpoints.py | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/response_api_endpoints/endpoints.py b/litellm/proxy/response_api_endpoints/endpoints.py index 110b627f36c..66db9b84dc8 100644 --- a/litellm/proxy/response_api_endpoints/endpoints.py +++ b/litellm/proxy/response_api_endpoints/endpoints.py @@ -2331,10 +2331,17 @@ async def _check_frame_may_modify_guardrails( team_object: Callable[[UserAPIKeyAuth], Awaitable[LiteLLM_TeamTable | None]], ) -> None: """A frame that picks guardrails or policies needs the team's permission to modify guardrails""" - if any("policies" in source or requests_guardrail_modification(source) for source in sources): + if any( + "policies" in source or requests_guardrail_modification(_without_client_metadata(source)) for source in sources + ): require_guardrail_modification_permission(await team_object(user_api_key_dict)) +def _without_client_metadata(source: Mapping[str, object]) -> Mapping[str, object]: + metadata: Final = source.get("metadata") + return {**source, "metadata": _metadata_picks("metadata", metadata)} if isinstance(metadata, dict) else source + + def _forwarded_frame_fields(source: Mapping[str, object]) -> dict[str, object]: return { key: _without_guardrail_selection(value) if key == "metadata" else value @@ -2422,7 +2429,11 @@ def _metadata_picks(key: str, metadata: object) -> Mapping[str, object]: validated: Final = _WS_FRAME.validate_python(metadata) if key != "metadata": return validated - return {name: item for name, item in validated.items() if not isinstance(item, str)} + return { + name: item + for name, item in validated.items() + if name not in _METADATA_GUARDRAIL_SELECTORS or not isinstance(item, str) + } def _frame_guardrail_selection( diff --git a/tests/unit/proxy/response_api_endpoints/test_endpoints.py b/tests/unit/proxy/response_api_endpoints/test_endpoints.py index fda037071ba..4d59c47aa3d 100644 --- a/tests/unit/proxy/response_api_endpoints/test_endpoints.py +++ b/tests/unit/proxy/response_api_endpoints/test_endpoints.py @@ -2577,18 +2577,23 @@ class TestResponsesWSFirstFrameModelAuth: @pytest.mark.asyncio async def test_frame_guard_forwards_string_metadata_named_like_a_pick_without_running_it(self): + from litellm.proxy._types import LiteLLM_TeamTable from litellm.proxy.response_api_endpoints.endpoints import _responses_ws_frame_guard key_required = _FrameGuardrail("key-required") frame_pick = _FrameGuardrail("frame-pick") metadata = {"guardrails": "frame-pick", "guardrail_config": "strict", "trace": "t1"} + async def locked_team(user_api_key_dict: UserAPIKeyAuth) -> LiteLLM_TeamTable: + return LiteLLM_TeamTable(team_id="team-1", metadata={"guardrails": {"modify_guardrails": False}}) + with _guardrails_on(key_required, frame_pick) as proxy_logging_obj: guard = _responses_ws_frame_guard( {"model": "gpt-4o-mini", "litellm_metadata": {"guardrails": ["key-required"]}}, _key(), proxy_logging_obj, MagicMock(), + team_object=locked_team, ) guarded = await guard(json.dumps({"type": "response.create", "input": "hi", "metadata": metadata})) @@ -2983,12 +2988,22 @@ class TestResponsesWSFirstFrameModelAuth: {"type": "response.create", "input": "hi", "guardrails": ["frame-pick"]}, {"type": "response.create", "input": "hi", "metadata": {"guardrails": ["frame-pick"]}}, {"type": "response.create", "input": "hi", "metadata": {"disable_global_guardrails": True}}, + {"type": "response.create", "input": "hi", "metadata": {"disable_global_guardrails": "true"}}, {"type": "response.create", "response": {"input": "hi", "guardrails": ["frame-pick"]}}, {"type": "response.create", "litellm_metadata": {"guardrails": ["frame-pick"]}, "response": {"input": "hi"}}, {"type": "response.create", "input": "hi", "policies": ["frame-policy"]}, {"type": "response.create", "response": {"input": "hi", "policies": ["frame-policy"]}}, ], - ids=["top-level", "metadata", "global-toggle", "nested", "beside-nested", "policies", "nested-policies"], + ids=[ + "top-level", + "metadata", + "global-toggle", + "string-global-toggle", + "nested", + "beside-nested", + "policies", + "nested-policies", + ], ) async def test_frame_guard_refuses_guardrail_and_policy_picks_from_a_team_without_permission(self, frame: dict[str, object]): from litellm.proxy._types import LiteLLM_TeamTable