Skip to content
51 changes: 51 additions & 0 deletions python/packages/core/agent_framework/security.py
Comment thread
moonbox3 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import math
import re
import uuid
from collections import OrderedDict
from collections.abc import Awaitable, Callable, Mapping, MutableMapping
from contextvars import ContextVar
from copy import copy, deepcopy
Expand Down Expand Up @@ -2022,6 +2023,7 @@ def __init__(
block_on_violation: bool = True,
enable_audit_log: bool = True,
approval_on_violation: bool = False,
max_pending_approvals: int | None = 1000,
) -> None:
"""Initialize PolicyEnforcementFunctionMiddleware.

Expand All @@ -2034,12 +2036,29 @@ def __init__(
when a policy violation is detected. If True, the middleware will return
a special result that triggers an approval request in the UI. After user
approval, the tool will execute with a warning about untrusted context.
max_pending_approvals: Maximum number of pending approvals to retain. When exceeded,
the oldest pending approval is evicted (FIFO). Set to None for no limit.
Defaults to 1000.
"""
if max_pending_approvals is not None and max_pending_approvals <= 0:
raise ValueError("max_pending_approvals must be None or a positive integer")

self.allow_untrusted_tools = allow_untrusted_tools or set()
self.approval_on_violation = approval_on_violation
# If approval_on_violation is True, we don't block - we request approval instead
self.block_on_violation = block_on_violation if not approval_on_violation else False
self.enable_audit_log = enable_audit_log
<<<<<<< HEAD
self.audit_log: list[dict[str, Any]] = []
self._max_pending_approvals = max_pending_approvals
# Track call_ids awaiting approval, each mapped to a binding record capturing the exact
# invocation the approval was requested for: the function name + arguments, the security
# label (integrity/confidentiality) shown for review, and the session. Combined with the
# call_id key and consume-on-use, an approval cannot re-authorize a repeated call, a
# different function, changed arguments, a different security label, or a different session.
# OrderedDict preserves insertion order for FIFO eviction when bounded.
self._pending_policy_approvals: OrderedDict[str, _PendingPolicyApproval] = OrderedDict()
=======
self._security_scope = _SecurityScope()

def _clone_for_scope(self, scope: _SecurityScope) -> PolicyEnforcementFunctionMiddleware:
Expand All @@ -2065,6 +2084,7 @@ def _get_pending_approval(self, approval_id: str) -> _PendingPolicyApproval | No
def _store_pending_approval(self, approval_id: str, record: _PendingPolicyApproval) -> None:
"""Store one pending approval in detached JSON-compatible form."""
self._pending_policy_approvals[approval_id] = record.to_state()
>>>>>>> upstream/main

def _get_call_id(self, context: FunctionInvocationContext) -> str:
"""Get the tool call id for this invocation context."""
Expand Down Expand Up @@ -2221,6 +2241,19 @@ def _matches_pending_approval(
pending = self._get_pending_approval(approval_id)
if pending is None:
return False

# Session-mismatch cleanup: if the pending entry is from a different session,
# it can never be consumed (approvals are session-bound). Remove it to prevent
# unbounded growth when call_ids are reused across sessions.
current_session_key = self._session_key(context)
if pending.session_key != current_session_key:
del self._pending_policy_approvals[approval_id]
logger.debug(
f"Removed stale pending approval '{approval_id}' from session '{pending.session_key}' "
f"(current session: '{current_session_key}')"
)
return False

approval_response = context.metadata.get("approval_response")
if not (
isinstance(approval_response, Content)
Expand Down Expand Up @@ -2276,9 +2309,27 @@ def _request_policy_violation_approval(
f"APPROVAL REQUESTED: Tool '{context.function.name}' requires user approval "
f"due to policy violation(s): {disclosed}."
)
call_id = self._get_call_id(context)
approval_id = self._get_approval_id(context)
if approval_id:
<<<<<<< HEAD
# If bounded, evict oldest entry when adding a new unique approval_id would exceed limit.
# Do not evict when updating an existing approval_id (re-request scenario).
if (
self._max_pending_approvals is not None
and approval_id not in self._pending_policy_approvals
and len(self._pending_policy_approvals) >= self._max_pending_approvals
):
# Evict oldest (first) entry
oldest_approval_id = next(iter(self._pending_policy_approvals))
del self._pending_policy_approvals[oldest_approval_id]
logger.debug(
f"Evicted oldest pending approval '{oldest_approval_id}' to maintain limit of {self._max_pending_approvals}"
)
self._pending_policy_approvals[approval_id] = self._pending_record(context, violations)
=======
self._store_pending_approval(approval_id, self._pending_record(context, violations))
>>>>>>> upstream/main
additional_properties: dict[str, Any] = {
"policy_violation": True,
"violation_type": primary["violation_type"],
Expand Down
Loading
Loading