Skip to content

chore(guardrails): tighten tool permission checks#26969

Merged
yuneng-berri merged 3 commits intoBerriAI:litellm_internal_stagingfrom
stuxf:codex/tool-permission-guardrail-fix
May 1, 2026
Merged

chore(guardrails): tighten tool permission checks#26969
yuneng-berri merged 3 commits intoBerriAI:litellm_internal_stagingfrom
stuxf:codex/tool-permission-guardrail-fix

Conversation

@stuxf
Copy link
Copy Markdown
Collaborator

@stuxf stuxf commented May 1, 2026

Relevant issues

Veria: VERIA-52

What changed

  • Normalize modern and legacy tool request shapes before ToolPermission pre-call checks.
  • Include legacy function-call responses in post-call permission enforcement.
  • Fail closed when parameter-rule arguments are missing or cannot be parsed.
  • Stabilize lazy OpenAPI snapshot operation IDs so CI verification is deterministic.

Tests

  • uv run pytest tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py -q
  • uv run pytest tests/test_litellm/proxy/guardrails/guardrail_hooks -q
  • uv run --python 3.12 pytest tests/test_litellm/proxy/test_lazy_openapi_snapshot.py tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py -q
  • uv run ruff check litellm/proxy/guardrails/guardrail_hooks/tool_permission.py tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py
  • uv run --python 3.12 ruff check litellm/proxy/_lazy_openapi_snapshot.py tests/test_litellm/proxy/test_lazy_openapi_snapshot.py litellm/proxy/guardrails/guardrail_hooks/tool_permission.py tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py
  • uv run black .
  • uv run --python 3.12 black --check litellm/proxy/_lazy_openapi_snapshot.py tests/test_litellm/proxy/test_lazy_openapi_snapshot.py litellm/proxy/guardrails/guardrail_hooks/tool_permission.py tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py
  • uv run --python 3.12 --no-sync mypy --no-incremental .
  • uv run --python 3.12 python -m litellm.proxy._lazy_openapi_snapshot stability check

@stuxf
Copy link
Copy Markdown
Collaborator Author

stuxf commented May 1, 2026

@greptileai

@codecov
Copy link
Copy Markdown

codecov Bot commented May 1, 2026

Codecov Report

❌ Patch coverage is 68.18182% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/_lazy_openapi_snapshot.py 68.18% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@stuxf
Copy link
Copy Markdown
Collaborator Author

stuxf commented May 1, 2026

@greptileai

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 1, 2026

Greptile Summary

This PR tightens the ToolPermissionGuardrail by normalizing both modern (tools/tool_choice) and legacy (functions/function_call) request shapes before pre-call checks, enforcing the same permission logic on legacy function_call responses post-call, and switching from a lenient to a fail-closed policy when a param-rule tool call has missing or unparseable arguments. It also stabilizes FastAPI-generated operation IDs in the lazy OpenAPI snapshot to make CI checks deterministic.

Confidence Score: 4/5

Safe to merge with awareness of the fail-closed behavior change for existing users using default_action=allow with param-rule tools.

All findings are P2. The logic is well-reasoned and thoroughly tested. The two flagged items — a narrow edge case in the operation-ID normalizer and the backwards-incompatible fail-closed argument check — are unlikely to affect most deployments but are worth discussing before merging.

litellm/proxy/guardrails/guardrail_hooks/tool_permission.py (fail-closed change) and litellm/proxy/_lazy_openapi_snapshot.py (custom-ID edge case)

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/tool_permission.py Core guardrail logic extended with legacy function-call normalization (pre- and post-call), fail-closed argument parsing, and new helper methods for extracting tool names from modern and legacy request shapes. Logic is sound and well-tested; one backwards-incompatible behavior change worth noting.
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py Extensive new tests covering legacy function-call pre/post hooks, rewrite-mode filtering, and fail-closed argument parsing; one test expectation was deliberately flipped from allow to block (previously discussed in thread).
litellm/proxy/_lazy_openapi_snapshot.py Adds _normalize_operation_ids to stabilize FastAPI-generated operation IDs for multi-method routes; works correctly for its stated purpose but could inadvertently rewrite custom IDs that happen to end with an HTTP method token.
tests/test_litellm/proxy/test_lazy_openapi_snapshot.py New unit tests for _normalize_operation_ids; covers the common case and custom ID preservation, but misses the edge case of custom IDs that end with an HTTP method token.
litellm/proxy/_lazy_openapi_snapshot.json Snapshot JSON updated to reflect the stabilized operation IDs produced by the new normalization logic.

Reviews (3): Last reviewed commit: "chore(proxy): stabilize lazy openapi sna..." | Re-trigger Greptile

Comment on lines +479 to +484
for forced_tool_name in (
self._get_named_tool_choice(data),
self._get_named_function_call(data),
):
if forced_tool_name is not None:
request_tools.append((forced_tool_name, "function"))
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 Duplicate permission checks for forced tool names

When a tool (e.g., "Read") appears in both tools/functions AND as a named tool_choice/function_call, _collect_request_tools adds it to request_tools more than once. In block mode this is harmless (raises on first denial), and in rewrite mode _modify_request_with_permission_errors converts denied_tool_names to a set, so no duplicate filtering occurs. However, _check_tool_permission is still called redundantly for the same tool, and if the permission check is ever stateful or expensive this could matter. Deduplicating the returned list (e.g., using dict.fromkeys) would make the contract cleaner.

Comment on lines +336 to +350
arguments, parse_error = self._parse_tool_call_arguments(tool_call)
if parse_error:
default_message = f"Tool '{tool_identifier}' {parse_error} required by rule '{rule.id}'"
message = self.render_violation_message(
default=default_message,
context={"tool_name": tool_identifier, "rule_id": rule.id},
)
return False, rule.id, message
if not arguments:
last_pattern_failure_msg = f"Tool '{tool_identifier}' is missing arguments required by rule '{rule.id}'"
continue
default_message = f"Tool '{tool_identifier}' is missing arguments required by rule '{rule.id}'"
message = self.render_violation_message(
default=default_message,
context={"tool_name": tool_identifier, "rule_id": rule.id},
)
return False, rule.id, message
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 if not arguments: branch reachable only for empty-dict arguments

After if parse_error: returns for all (None, error_msg) outcomes, the subsequent if not arguments: is only reached when _parse_tool_call_arguments returns ({}, None) — i.e., when the JSON decoded to an empty dict. The guard is correct and intentional (fail closed), but the message "is missing arguments required by rule" is slightly misleading for this case since arguments are present but empty. A message like "has no argument values required by rule" would distinguish this from the actual missing-arguments path and aid debugging.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@stuxf
Copy link
Copy Markdown
Collaborator Author

stuxf commented May 1, 2026

@greptileai

Bojun-Vvibe added a commit to Bojun-Vvibe/oss-contributions that referenced this pull request May 1, 2026
- BerriAI/litellm#26969: tool-permission guardrail tightening (merge-after-nits)
- BerriAI/litellm#26967: VCR Redis observability (merge-as-is)
- google-gemini/gemini-cli#26303: brain/critique role split + iteration (needs-discussion)
- google-gemini/gemini-cli#26287: voice transcription cursor-position insert (merge-after-nits)
- google-gemini/gemini-cli#26274: ssh:// extension install scheme (merge-as-is)
@stuxf stuxf marked this pull request as ready for review May 1, 2026 08:18
@yuneng-berri yuneng-berri merged commit 42122b8 into BerriAI:litellm_internal_staging May 1, 2026
44 checks passed
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.

2 participants