Skip to content

fix(guardrails): recompile ToolPermissionGuardrail rules on update_in_memory_litellm_params#29613

Closed
VANDRANKI wants to merge 1 commit into
BerriAI:mainfrom
VANDRANKI:fix/tool-permission-guardrail-recompile-on-update
Closed

fix(guardrails): recompile ToolPermissionGuardrail rules on update_in_memory_litellm_params#29613
VANDRANKI wants to merge 1 commit into
BerriAI:mainfrom
VANDRANKI:fix/tool-permission-guardrail-recompile-on-update

Conversation

@VANDRANKI

Copy link
Copy Markdown
Contributor

Summary

Fixes #29592.

ToolPermissionGuardrail compiles _compiled_rule_targets and _compiled_rule_patterns only inside __init__. The base-class update_in_memory_litellm_params updates self.rules via setattr but never rebuilds the compiled maps. Any rule change sent through PUT /guardrails is accepted but silently ignored at enforcement time until the process reinitializes the guardrail (restart, PATCH path, or DB poll).

PresidioGuardrail has the same derived-state problem and already overrides update_in_memory_litellm_params to re-apply its state. This PR mirrors that pattern for ToolPermissionGuardrail.

Changes

  • Extract the rule-compilation loop from __init__ into a private _recompile_rules(self, rules) helper.
  • __init__ now calls self._recompile_rules(rules) (behaviour unchanged).
  • New update_in_memory_litellm_params override: calls super(), then calls self._recompile_rules(litellm_params.rules) whenever rules are updated, and also re-normalises default_action / on_disallowed_action from the incoming params.

Test plan

  • Reproduce the script from the issue: after update_in_memory_litellm_params, _compiled_rule_patterns should contain the new pattern and _get_permission_for_tool_call should enforce it
  • Existing behavior: constructing ToolPermissionGuardrail directly with rules still works (rules compiled in __init__)
  • Calling update_in_memory_litellm_params with rules=None does not wipe existing compiled state

@codspeed-hq

codspeed-hq Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing VANDRANKI:fix/tool-permission-guardrail-recompile-on-update (611d3ac) with main (5be0797)

Open in CodSpeed

@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.85366% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...roxy/guardrails/guardrail_hooks/tool_permission.py 65.85% 14 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes silent stale-state enforcement in ToolPermissionGuardrail by extracting the rule-compilation loop into _recompile_rules and overriding update_in_memory_litellm_params so that a PUT /guardrails call triggers a recompile — mirroring the pattern already used by PresidioGuardrail.

  • _recompile_rules replaces the inline compilation block in __init__, resetting and rebuilding self.rules, _compiled_rule_patterns, and _compiled_rule_targets.
  • update_in_memory_litellm_params calls super() then conditionally recompiles rules and re-normalises default_action/on_disallowed_action from incoming params.
  • The override has a gap: super() unconditionally sets self.rules = None when litellm_params.rules is absent (its Pydantic default), and the subsequent if litellm_params.rules is not None: guard then skips _recompile_rules, leaving self.rules as None and causing a TypeError at the next tool-check iteration.

Confidence Score: 3/5

The change introduces a regression in the override: when PUT /guardrails is called without an explicit rules field, the base-class setattr loop clobbers self.rules with None, and the new guard skips the recompile step, leaving the guardrail in a broken state that crashes on the next tool-permission check.

The core refactor is sound and the intent is correct, but the ordering of super() before the None-guard means a routine PUT /guardrails payload that omits rules silently destroys self.rules, causing every subsequent tool request to throw TypeError. This is a real runtime breakage on the changed code path.

litellm/proxy/guardrails/guardrail_hooks/tool_permission.py — specifically the update_in_memory_litellm_params method around the super() call and the rules is not None guard.

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/tool_permission.py Extracts rule compilation into _recompile_rules and adds update_in_memory_litellm_params override, but the override has a defect: calling super() first lets the base class set self.rules = None (the Pydantic default) when no rules are provided, and the subsequent guard skips _recompile_rules, leaving self.rules as None and causing a TypeError on the next tool-permission check.

Reviews (1): Last reviewed commit: "fix(guardrails): recompile ToolPermissio..." | Re-trigger Greptile

Comment on lines +148 to +150
super().update_in_memory_litellm_params(litellm_params)
if litellm_params.rules is not None:
self._recompile_rules(litellm_params.rules)

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.

P1 self.rules left as None when litellm_params.rules is not provided

The base-class super().update_in_memory_litellm_params(litellm_params) iterates vars(litellm_params) and calls setattr(self, key, value) for every field — including rules, which defaults to None in ToolPermissionGuardrailConfigModel. When a PUT /guardrails call does not include rules, litellm_params.rules is None, so super() sets self.rules = None and then the guard if litellm_params.rules is not None: skips _recompile_rules. The next incoming request hits for rule in self.rules: in _check_tool_permission or _get_permission_for_tool_call and raises TypeError: 'NoneType' object is not iterable, crashing enforcement entirely. A minimal safeguard like if not isinstance(self.rules, list): self.rules = [] after the super call (or calling _recompile_rules unconditionally with litellm_params.rules or []) would prevent this.

Comment on lines 63 to +66
self.rules: List[ToolPermissionRule] = []
self._compiled_rule_patterns: Dict[str, Dict[str, re.Pattern]] = {}
self._compiled_rule_targets: Dict[str, Dict[str, Optional[re.Pattern]]] = {}
if rules:
for rule_item in rules:
if isinstance(rule_item, ToolPermissionRule):
rule = rule_item
else:
rule = ToolPermissionRule(**rule_item)
self.rules.append(rule)

compiled_target_patterns: Dict[str, Optional[re.Pattern]] = {
"tool_name": None,
"tool_type": None,
}
if rule.tool_name is not None:
try:
compiled_target_patterns["tool_name"] = re.compile(
rule.tool_name
)
except re.error as exc:
raise ValueError(
f"Invalid regex for tool_name in rule '{rule.id}': {exc}"
) from exc
if rule.tool_type is not None:
try:
compiled_target_patterns["tool_type"] = re.compile(
rule.tool_type
)
except re.error as exc:
raise ValueError(
f"Invalid regex for tool_type in rule '{rule.id}': {exc}"
) from exc
self._compiled_rule_targets[rule.id] = compiled_target_patterns

if rule.allowed_param_patterns:
compiled_patterns: Dict[str, re.Pattern] = {}
for path, pattern in rule.allowed_param_patterns.items():
try:
compiled_patterns[path] = re.compile(pattern)
except re.error as exc:
raise ValueError(
f"Invalid regex in allowed_param_patterns for rule '{rule.id}': {exc}"
) from exc

if compiled_patterns:
self._compiled_rule_patterns[rule.id] = compiled_patterns
self._recompile_rules(rules)

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 The three explicit assignments on lines 63-65 are immediately overwritten by the _recompile_rules call that follows — that helper already resets all three attributes at its top. These lines are redundant.

Suggested change
self.rules: List[ToolPermissionRule] = []
self._compiled_rule_patterns: Dict[str, Dict[str, re.Pattern]] = {}
self._compiled_rule_targets: Dict[str, Dict[str, Optional[re.Pattern]]] = {}
if rules:
for rule_item in rules:
if isinstance(rule_item, ToolPermissionRule):
rule = rule_item
else:
rule = ToolPermissionRule(**rule_item)
self.rules.append(rule)
compiled_target_patterns: Dict[str, Optional[re.Pattern]] = {
"tool_name": None,
"tool_type": None,
}
if rule.tool_name is not None:
try:
compiled_target_patterns["tool_name"] = re.compile(
rule.tool_name
)
except re.error as exc:
raise ValueError(
f"Invalid regex for tool_name in rule '{rule.id}': {exc}"
) from exc
if rule.tool_type is not None:
try:
compiled_target_patterns["tool_type"] = re.compile(
rule.tool_type
)
except re.error as exc:
raise ValueError(
f"Invalid regex for tool_type in rule '{rule.id}': {exc}"
) from exc
self._compiled_rule_targets[rule.id] = compiled_target_patterns
if rule.allowed_param_patterns:
compiled_patterns: Dict[str, re.Pattern] = {}
for path, pattern in rule.allowed_param_patterns.items():
try:
compiled_patterns[path] = re.compile(pattern)
except re.error as exc:
raise ValueError(
f"Invalid regex in allowed_param_patterns for rule '{rule.id}': {exc}"
) from exc
if compiled_patterns:
self._compiled_rule_patterns[rule.id] = compiled_patterns
self._recompile_rules(rules)
self._recompile_rules(rules)

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!

@Dinesh-Girbide

Copy link
Copy Markdown
Contributor

Heads-up to avoid duplicated effort: this targets the same bug as #29592. I opened #29655 against litellm_oss_branch with the same core fix (extract the compile step into a helper and override update_in_memory_litellm_params), plus a few things that are tripping CI on this PR:

  • it imports LitellmParams (the lint F821 failure here),
  • it reads litellm_params dict-or-model safely (litellm_params if isinstance(litellm_params, dict) else vars(litellm_params)), so the recompile also works when the proxy hands the raw DB dict to the in-memory sync on the PUT /guardrails path, and
  • it adds regression tests (for codecov/patch).

Also note the "Verify PR source branch" check requires external/fork PRs to target litellm_oss_branch, not main. Totally happy for maintainers to take whichever, or to fold these adjustments into this PR; flagging so we don't double up.

@Sameerlite Sameerlite closed this Jun 4, 2026
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.

3 participants