fix(agents): mask static_headers values for every caller

Co-authored-by: Parman Mohammadalizadeh <prmma23@gmail.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Moe Khalil 2026-10-31 18:40:27 +00:00
parent 3891443125
commit f667a4b1e6
2 changed files with 54 additions and 8 deletions

View file

@ -73,6 +73,9 @@ class AgentRecord(Protocol):
@property
def agent_name(self) -> str: ...
@property
def static_headers(self) -> object: ...
@property
def litellm_params(self) -> Mapping[str, object] | None: ...
@ -280,6 +283,43 @@ def parse_agent_litellm_params(value: object) -> Mapping[str, object]:
_KILL_SWITCH_ADAPTER: Final[TypeAdapter[AgentKillSwitchConfig | None]] = TypeAdapter(AgentKillSwitchConfig | None)
_AGENT_STATIC_HEADERS_ADAPTER: Final[TypeAdapter[Mapping[str, str]]] = TypeAdapter(Mapping[str, str])
_EMPTY_AGENT_STATIC_HEADERS: Final[Mapping[str, str]] = MappingProxyType({})
def redact_agent_static_headers(
headers: Mapping[str, str] | None,
) -> dict[str, str] | None: # mutable-ok: AgentResponse.static_headers is a plain dict
if headers is None:
return None
return {name: REDACTED_BY_LITELM_STRING for name in headers}
def parse_agent_static_headers(value: object) -> Mapping[str, str]:
if value is None:
return _EMPTY_AGENT_STATIC_HEADERS
try:
parsed: Final = (
_AGENT_STATIC_HEADERS_ADAPTER.validate_json(value)
if isinstance(value, str)
else _AGENT_STATIC_HEADERS_ADAPTER.validate_python(value)
)
except ValidationError:
return _EMPTY_AGENT_STATIC_HEADERS
return MappingProxyType(dict(parsed))
def serialize_agent_static_headers(incoming: Mapping[str, str] | None, existing: object) -> str:
if incoming is None:
return safe_dumps({})
existing_headers: Final = parse_agent_static_headers(existing)
return safe_dumps(
{
name: existing_headers[name] if value == REDACTED_BY_LITELM_STRING else value
for name, value in incoming.items()
if value != REDACTED_BY_LITELM_STRING or name in existing_headers
}
)
def parse_agent_kill_switch(value: object) -> AgentKillSwitchConfig | None:
@ -615,7 +655,9 @@ class AgentRegistry:
# Serialize static_headers
static_headers_obj: Final = agent.get("static_headers")
static_headers_val: Final[str | None] = safe_dumps(dict(static_headers_obj)) if static_headers_obj else None
static_headers_val: Final[str | None] = (
serialize_agent_static_headers(static_headers_obj, None) if static_headers_obj else None
)
extra_headers_val: Final = agent.get("extra_headers")
access_group_ids_val: Final = agent.get("access_group_ids")
@ -751,7 +793,9 @@ class AgentRegistry:
update_data[rate_field] = agent.get(rate_field)
if "static_headers" in agent:
headers_value: Final = agent.get("static_headers")
update_data["static_headers"] = safe_dumps(dict(headers_value) if headers_value is not None else {})
update_data["static_headers"] = serialize_agent_static_headers(
headers_value, existing_agent.get("static_headers")
)
if "extra_headers" in agent:
extra_headers_value: Final = agent.get("extra_headers")
update_data["extra_headers"] = extra_headers_value if extra_headers_value is not None else []
@ -828,8 +872,8 @@ class AgentRegistry:
# Serialize static_headers for update
static_headers_obj_u: Final = agent.get("static_headers")
static_headers_val_u: Final[str] = (
safe_dumps(dict(static_headers_obj_u)) if static_headers_obj_u is not None else safe_dumps({})
static_headers_val_u: Final[str] = serialize_agent_static_headers(
static_headers_obj_u, existing_row.static_headers if existing_row is not None else None
)
extra_headers_val_u: Final = agent.get("extra_headers") or []
access_group_ids_val_u: Final = tuple(dict.fromkeys(agent.get("access_group_ids") or ()))

View file

@ -38,6 +38,7 @@ from litellm.proxy.agent_endpoints.agent_registry import (
AgentIdWhere,
parse_agent_kill_switch,
parse_agent_litellm_params,
redact_agent_static_headers,
redact_sensitive_agent_litellm_params,
)
from litellm.proxy.agent_endpoints.agent_search import (
@ -187,10 +188,10 @@ def _redact_sensitive_agent_fields(
) -> list[AgentResponse]:
"""
Return copies of the given agents with credential-bearing litellm_params
values and kill-switch auth secrets replaced by a fixed marker (never
returned to ANY caller, admin included) and, for non-admin callers,
virtual-key, header and kill-switch fields stripped entirely. The original
objects are not modified.
values, static-header values and kill-switch auth secrets replaced by a
fixed marker (never returned to ANY caller, admin included) and, for
non-admin callers, virtual-key, header and kill-switch fields stripped
entirely. The original objects are not modified.
"""
from litellm.proxy.proxy_server import general_settings, jwt_handler
@ -207,6 +208,7 @@ def _redact_sensitive_agent_fields(
copy.keys = None
copy.kill_switch = None
copy.identity = None
copy.static_headers = redact_agent_static_headers(copy.static_headers)
if copy.litellm_params:
copy.litellm_params = _redact_agent_litellm_params_dict(copy.litellm_params)
copy.kill_switch = redact_kill_switch(copy.kill_switch)