fix(responses): ask no team permission for string metadata named like a guardrail pick on socket turns

This commit is contained in:
mateo-berri 2026-10-29 18:31:48 -07:00
parent 119c67f655
commit 18d96358d6
2 changed files with 29 additions and 3 deletions

View file

@ -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(

View file

@ -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