Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions python/packages/core/agent_framework/_harness/_tool_approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,24 @@ def _function_call_from_request(request: Content) -> Content | None:
function_call = request.function_call
if function_call is None or function_call.type != "function_call" or function_call.name is None:
return None
request_props = request.additional_properties or {}
if request_props:
function_call = copy.copy(function_call)
function_call.additional_properties = {
**(function_call.additional_properties or {}),
**request_props,
}
return function_call


def _has_policy_violation(request: Content) -> bool:
"""Return whether an approval request represents a FIDES policy violation."""
properties = request.additional_properties or {}
return bool(
properties.get("policy_violation") or properties.get("blocked_violation") or properties.get("_fides_violations")
)


def _arguments_match(rule_arguments: Mapping[str, str], function_call: Content) -> bool:
call_arguments = _serialize_arguments(function_call) or {}
if len(rule_arguments) != len(call_arguments):
Expand Down Expand Up @@ -557,7 +572,9 @@ def _inject_collected_responses(self, messages: Sequence[Message], state: ToolAp
async def _drain_auto_approvable_queue(self, state: ToolApprovalState) -> None:
remaining: list[Content] = []
for request in state.queued_approval_requests:
if _matches_rule(request, state.rules) or await self._matches_auto_rule(request):
if not _has_policy_violation(request) and (
_matches_rule(request, state.rules) or await self._matches_auto_rule(request)
):
state.collected_approval_responses.append(request.to_function_approval_response(approved=True))
continue
remaining.append(request)
Expand All @@ -581,7 +598,9 @@ async def _process_outbound_messages(self, messages: list[Message], state: ToolA
auto_approved: set[int] = set()
unresolved: list[Content] = []
for request in approval_requests:
if _matches_rule(request, state.rules) or await self._matches_auto_rule(request):
if not _has_policy_violation(request) and (
_matches_rule(request, state.rules) or await self._matches_auto_rule(request)
):
state.collected_approval_responses.append(request.to_function_approval_response(approved=True))
auto_approved.add(id(request))
else:
Expand Down
29 changes: 21 additions & 8 deletions python/packages/core/agent_framework/_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1603,19 +1603,28 @@ async def final_function_handler(context_obj: Any) -> Any:
return Content.from_function_result(call_id=call_id, result=function_result)
except MiddlewareTermination as term_exc:
# Re-raise to signal loop termination, but first capture any result set by middleware
if middleware_context.result is not None:
middleware_result = middleware_context.result
if middleware_result is not None:
blocked_result = cast(dict[str, Any], middleware_result) if isinstance(middleware_result, dict) else None
if blocked_result is not None and blocked_result.get("blocked_violation") is True:
blocked_properties = dict(function_call_content.additional_properties or {})
blocked_properties.update({key: value for key, value in blocked_result.items() if key != "error"})
blocked_error = str(blocked_result.get("error", "Tool blocked by security policy."))
term_exc.result = Content.from_function_result(
call_id=call_id,
result=blocked_error,
exception=blocked_error,
additional_properties=blocked_properties,
)
# Pass through function_approval_request directly (e.g., from security policy middleware)
# so the approval flow in _handle_function_call_results activates correctly.
if (
isinstance(middleware_context.result, Content)
and middleware_context.result.type == "function_approval_request"
):
term_exc.result = middleware_context.result
elif isinstance(middleware_result, Content) and middleware_result.type == "function_approval_request":
term_exc.result = middleware_result
else:
# Store result in exception for caller to extract
term_exc.result = Content.from_function_result(
call_id=call_id,
result=middleware_context.result,
result=middleware_result,
additional_properties=function_call_content.additional_properties,
)
raise
Expand Down Expand Up @@ -1671,7 +1680,11 @@ async def _execute_single_function_call(
return [result], False
except MiddlewareTermination as exc:
if isinstance(exc.result, Content):
return [exc.result], True
# A blocked FIDES call is a normal tool result: the model must receive
# the refusal and get a chance to explain or choose another action.
# Approval requests remain terminal and pause for user input.
is_blocked_policy = (exc.result.additional_properties or {}).get("blocked_violation") is True
return [exc.result], not is_blocked_policy
source_function_call = _underlying_function_call(function_call)
return [
Content.from_function_result(
Expand Down
Loading
Loading