Skip to content

[Fix] Agent endpoint and routing permission checks#25922

Merged
yuneng-berri merged 8 commits intolitellm_yj_apr17from
litellm_a2a_agent_acl
Apr 18, 2026
Merged

[Fix] Agent endpoint and routing permission checks#25922
yuneng-berri merged 8 commits intolitellm_yj_apr17from
litellm_a2a_agent_acl

Conversation

@yuneng-berri
Copy link
Copy Markdown
Collaborator

Summary

  • The GET /v1/agents/{agent_id} endpoint verified feature access but did not enforce per-agent ACLs. Any authenticated user with the agents feature enabled could fetch the full configuration of any agent.
  • The route_a2a_agent_request helper, used when routing a2a/<name> model strings through /v1/chat/completions, had no permission check before forwarding to the backend.

Changes

get_agent_by_id now calls AgentRequestHandler.is_agent_allowed after the feature-flag check, returning 403 if the caller's key/team does not include the requested agent in its allowed list.

route_a2a_agent_request is now async and accepts an optional user_api_key_dict. It calls AgentRequestHandler.is_agent_allowed before injecting api_base and forwarding the request. route_request in route_llm_request.py now accepts and passes through user_api_key_dict, and base_process_llm_request forwards it from the handler. Both changes follow the pattern already used in invoke_agent_a2a and get_agent_card.

Testing

  • Verified GET /v1/agents/{id} returns 403 for a key whose object_permission.agents does not include the requested agent (was 200 before).
  • Verified admin key still gets 200 with full static_headers.
  • Verified key with the agent in its allowed list still gets 200.
  • Verified unrestricted keys (no object_permission) still get 200 on any agent.
  • uv run pytest tests/test_litellm/proxy/agent_endpoints/ tests/test_litellm/proxy/test_route_a2a_models.py -v — 65 passed.

Type

🐛 Bug Fix
✅ Test

…headers and add tests

- Move protected-headers set to module level as a frozenset
- Add x-api-key, x-goog-api-key to protected set (provider credential headers)
- Block x-amz- prefix to cover AWS SigV4 signing headers
- Normalize forwarded header names to lowercase on write
- Log at debug level when a protected header is skipped
- Add unit test covering protected-header drop and non-protected forwarding
[Fix] Restrict x-pass- header forwarding for credential and protocol headers
[Fix] Tighten api_key value check in credential validation
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 17, 2026 4:13am

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 17, 2026

Greptile Summary

This PR adds per-agent ACL enforcement to two previously unguarded paths: GET /v1/agents/{agent_id} (which performed only a feature-flag check) and route_a2a_agent_request (which forwarded a2a/<name> completion requests with no permission check at all). Both paths now call AgentRequestHandler.is_agent_allowed for non-admin users, following the pattern already established in invoke_agent_a2a and get_agent_card. All other changes in common_request_processing.py are Black formatting only.

Confidence Score: 5/5

Safe to merge — the permission checks are correctly implemented, admin bypass is consistent, and all remaining considerations are pre-existing or P2.

No P0 or P1 findings. Both new ACL check sites correctly call is_agent_allowed for non-admins, return 403 on denial, and allow all agents when the allowed list is empty (unrestricted keys). The route_request signature change is backwards-compatible (Optional with default None). The async conversion of route_a2a_agent_request is consistent with the surrounding coroutine-return pattern used throughout route_request.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/agent_endpoints/a2a_routing.py Converted to async and added is_agent_allowed check before injecting api_base; admin shortcut correctly guards the None case; pre-existing fail-open on DB error is noted in prior threads.
litellm/proxy/agent_endpoints/endpoints.py get_agent_by_id now enforces ACL for non-admins after the feature-flag check; remaining changes are Black reformats of multi-line assignments.
litellm/proxy/common_request_processing.py Sole functional change: threads user_api_key_dict through to route_request; all other diffs are Black formatting.
litellm/proxy/route_llm_request.py Adds optional user_api_key_dict param to route_request and forwards it to the now-awaited route_a2a_agent_request; backwards-compatible change.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Auth as user_api_key_auth
    participant EP as endpoints.py get_agent_by_id
    participant CR as common_request_processing
    participant RR as route_request
    participant A2A as route_a2a_agent_request
    participant APH as AgentRequestHandler is_agent_allowed
    participant LiteLLM

    Client->>Auth: GET /v1/agents/{id} or POST /v1/chat/completions
    Auth-->>EP: user_api_key_dict

    alt GET /v1/agents/{id}
        EP->>EP: check_feature_access_for_user
        EP->>EP: is_admin check
        alt not admin
            EP->>APH: is_agent_allowed(agent_id, user_api_key_dict)
            APH-->>EP: True / False
            EP->>EP: raise 403 if not allowed
        end
        EP-->>Client: AgentResponse
    else POST /v1/chat/completions with a2a/ model
        CR->>RR: route_request(..., user_api_key_dict)
        RR->>RR: _is_a2a_agent_model check
        RR->>A2A: await route_a2a_agent_request(data, route_type, user_api_key_dict)
        A2A->>A2A: is_admin check
        alt not admin
            A2A->>APH: is_agent_allowed(agent_id, user_api_key_dict)
            APH-->>A2A: True / False
            A2A->>A2A: raise 403 if not allowed
        end
        A2A->>LiteLLM: litellm.route_type with api_base injected
        LiteLLM-->>Client: completion response
    end
Loading

Reviews (2): Last reviewed commit: "fix: add explicit admin bypass to agent ..." | Re-trigger Greptile

Comment on lines +399 to +406
is_allowed = await AgentRequestHandler.is_agent_allowed(
agent_id=agent_id, user_api_key_auth=user_api_key_dict
)
if not is_allowed:
raise HTTPException(
status_code=403,
detail=f"Agent '{agent_id}' is not allowed for your key/team. Contact proxy admin for access.",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Admin bypass not explicit

get_agents (the list endpoint) has an explicit admin shortcut that skips the ACL entirely. get_agent_by_id calls is_agent_allowed for all users, relying on the implicit behaviour that admins have no object_permission so get_allowed_agents returns [] → allow all. In practice both paths produce the same result, but if an admin key were ever created with a non-empty object_permission.agents, get_agent_by_id would 403 them while GET /v1/agents would still return the full list. Consider adding the same explicit guard used in the list endpoint for consistency:

if (
    user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
    or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
):
    pass  # admin always allowed
else:
    is_allowed = await AgentRequestHandler.is_agent_allowed(...)
    if not is_allowed:
        raise HTTPException(status_code=403, ...)

Comment on lines +53 to +62
# Verify the caller is permitted to use this agent
is_allowed = await AgentRequestHandler.is_agent_allowed(
agent_id=agent.agent_id,
user_api_key_auth=user_api_key_dict,
)
if not is_allowed:
raise HTTPException(
status_code=403,
detail=f"Agent '{agent_name}' is not allowed for your key/team. Contact proxy admin for access.",
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fail-open on permission-lookup exception

AgentRequestHandler.get_allowed_agents swallows all exceptions and returns [], which is_agent_allowed treats as "no restrictions → allow all". If the DB or cache is unavailable while fetching team permissions, every A2A request silently passes the ACL check. This is pre-existing behaviour in the permission handler and is now active on the critical completion path. Consider whether a DB-error case should deny rather than allow, or at least emit a metric to make the silent pass-through observable.

@yuneng-berri yuneng-berri temporarily deployed to integration-postgres April 17, 2026 04:12 — with GitHub Actions Inactive
@yuneng-berri yuneng-berri temporarily deployed to integration-postgres April 17, 2026 04:12 — with GitHub Actions Inactive
@yuneng-berri yuneng-berri temporarily deployed to integration-postgres April 17, 2026 04:12 — with GitHub Actions Inactive
Base automatically changed from litellm_yj_apr16 to litellm_internal_staging April 17, 2026 21:58
@yuneng-berri yuneng-berri changed the base branch from litellm_internal_staging to litellm_yj_apr17 April 18, 2026 00:19
@yuneng-berri yuneng-berri merged commit b9f5be8 into litellm_yj_apr17 Apr 18, 2026
96 of 100 checks passed
@yuneng-berri yuneng-berri deleted the litellm_a2a_agent_acl branch April 18, 2026 00:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant