Description
Summary
PolicyEnforcementFunctionMiddleware can retain pending policy-approval entries indefinitely when a policy violation occurs with approval_on_violation=True but the corresponding approval is never successfully consumed.
Each pending approval is stored in self._pending_policy_approvals keyed by the tool call's call_id. The entry is removed by _consume_pending_approval() when the corresponding approval is successfully consumed, but there is no cleanup path for approvals that are rejected, ignored, abandoned, or otherwise never consumed.
Because middleware instances can be long-lived and reused across many agent interactions, this can result in unbounded memory growth over the lifetime of the middleware instance.
Expected behavior
Pending approval state should have a bounded lifetime.
If an approval is rejected, abandoned, expires, or the relevant interaction/session ends, the associated pending approval should eventually be removed.
Possible implementation approaches include:
- TTL-based expiration
- A maximum number of pending approvals with eviction
- Cleanup tied to the appropriate session/interaction lifecycle
The exact approach can be determined based on the intended lifecycle semantics of the middleware.
Actual behavior
self._pending_policy_approvals grows for every policy violation whose approval is not successfully consumed.
There is currently no TTL, maximum-size bound, or general cleanup mechanism for entries that are never consumed.
Each _PendingPolicyApproval entry retains more state than just the call_id, including values such as body_signature, label_key, session_key, and disclosed_violations.
Therefore, a long-lived middleware instance can continuously accumulate pending approval state as additional unconsumed policy violations occur.
Impact
The primary impact is availability/reliability through unbounded memory retention.
Applications that:
- reuse a
PolicyEnforcementFunctionMiddleware instance across many interactions;
- enable
approval_on_violation=True; and
- encounter policy violations whose approvals are never subsequently consumed
can experience sustained growth in process memory.
At sufficient scale, this could contribute to memory exhaustion and process instability.
The affected component is also part of the policy-enforcement/security path, so ensuring that abandoned approval state cannot accumulate without bound seems important.
Related issue
This appears distinct from #6966.
#6966 addressed binding an approval to the correct tool invocation and preventing an approval from being incorrectly reused for a different invocation.
This issue concerns the lifecycle of the pending approval itself: when an approval is never successfully consumed, the corresponding pending state remains retained.
Experimental feature
PolicyEnforcementFunctionMiddleware is currently marked as an experimental FIDES feature. This issue was reproduced with that implementation enabled.
I am reporting it with that context in mind so the experimental status is clear for triage.
Code Sample
"""
Tested against:
- agent-framework-core 1.15.0
- Python 3.13.15
"""
import asyncio
from agent_framework import (
FunctionInvocationContext,
FunctionTool,
MiddlewareTermination,
)
from agent_framework.security import (
ContentLabel,
IntegrityLabel,
PolicyEnforcementFunctionMiddleware,
)
async def dangerous_tool(x: str) -> str:
return x
async def call_next() -> None:
# Never reached: every invocation is blocked pending approval.
pass
async def main() -> None:
tool = FunctionTool(
name="dangerous_tool",
description="d",
func=dangerous_tool,
)
middleware = PolicyEnforcementFunctionMiddleware(
approval_on_violation=True,
)
N = 5000
for i in range(N):
ctx = FunctionInvocationContext(
function=tool,
arguments={"x": "irrelevant"},
metadata={
"call_id": f"call_{i}",
"context_label": ContentLabel(
integrity=IntegrityLabel.UNTRUSTED,
),
},
)
try:
await middleware.process(ctx, call_next)
except MiddlewareTermination:
# Expected: the policy violation requests approval.
# No caller ever supplies an approval for this call_id.
pass
print("Tool calls simulated :", N)
print(
"Entries still pending :",
len(middleware._pending_policy_approvals),
)
if __name__ == "__main__":
asyncio.run(main())
Error Messages / Stack Traces
No exception or stack trace is produced.
The issue manifests as unbounded growth of the middleware's pending-approval state rather than as an immediate runtime error.
Package Versions
agent-framework-core: 1.15.0
Python Version
Python 3.13.15
Additional Context
The relevant lifecycle appears to be:
Policy violation
|
v
_request_policy_violation_approval()
|
v
_pending_policy_approvals[call_id] = pending_approval
|
v
Approval successfully consumed?
|
+-- Yes --> _consume_pending_approval() --> entry removed
|
+-- No --> no corresponding cleanup path
The reproduction intentionally never provides an approval for any generated call_id.
With 5,000 policy-violating invocations, the middleware retains 5,000 pending approval entries.
Observed output:
Tool calls simulated : 5000
Entries still pending : 5000
The reproduction was run against the real library implementation rather than a mocked or modified version of the vulnerable middleware.
I would be happy to provide a focused regression test and/or a PR for the fix if the maintainers agree with the expected lifecycle behavior.
Description
Summary
PolicyEnforcementFunctionMiddlewarecan retain pending policy-approval entries indefinitely when a policy violation occurs withapproval_on_violation=Truebut the corresponding approval is never successfully consumed.Each pending approval is stored in
self._pending_policy_approvalskeyed by the tool call'scall_id. The entry is removed by_consume_pending_approval()when the corresponding approval is successfully consumed, but there is no cleanup path for approvals that are rejected, ignored, abandoned, or otherwise never consumed.Because middleware instances can be long-lived and reused across many agent interactions, this can result in unbounded memory growth over the lifetime of the middleware instance.
Expected behavior
Pending approval state should have a bounded lifetime.
If an approval is rejected, abandoned, expires, or the relevant interaction/session ends, the associated pending approval should eventually be removed.
Possible implementation approaches include:
The exact approach can be determined based on the intended lifecycle semantics of the middleware.
Actual behavior
self._pending_policy_approvalsgrows for every policy violation whose approval is not successfully consumed.There is currently no TTL, maximum-size bound, or general cleanup mechanism for entries that are never consumed.
Each
_PendingPolicyApprovalentry retains more state than just thecall_id, including values such asbody_signature,label_key,session_key, anddisclosed_violations.Therefore, a long-lived middleware instance can continuously accumulate pending approval state as additional unconsumed policy violations occur.
Impact
The primary impact is availability/reliability through unbounded memory retention.
Applications that:
PolicyEnforcementFunctionMiddlewareinstance across many interactions;approval_on_violation=True; andcan experience sustained growth in process memory.
At sufficient scale, this could contribute to memory exhaustion and process instability.
The affected component is also part of the policy-enforcement/security path, so ensuring that abandoned approval state cannot accumulate without bound seems important.
Related issue
This appears distinct from #6966.
#6966 addressed binding an approval to the correct tool invocation and preventing an approval from being incorrectly reused for a different invocation.
This issue concerns the lifecycle of the pending approval itself: when an approval is never successfully consumed, the corresponding pending state remains retained.
Experimental feature
PolicyEnforcementFunctionMiddlewareis currently marked as an experimental FIDES feature. This issue was reproduced with that implementation enabled.I am reporting it with that context in mind so the experimental status is clear for triage.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.15.0
Python Version
Python 3.13.15
Additional Context
The relevant lifecycle appears to be:
Policy violation
|
v
_request_policy_violation_approval()
|
v
_pending_policy_approvals[call_id] = pending_approval
|
v
Approval successfully consumed?
|
+-- Yes --> _consume_pending_approval() --> entry removed
|
+-- No --> no corresponding cleanup path
The reproduction intentionally never provides an approval for any generated
call_id.With 5,000 policy-violating invocations, the middleware retains 5,000 pending approval entries.
Observed output:
Tool calls simulated : 5000
Entries still pending : 5000
The reproduction was run against the real library implementation rather than a mocked or modified version of the vulnerable middleware.
I would be happy to provide a focused regression test and/or a PR for the fix if the maintainers agree with the expected lifecycle behavior.