Description
In PolicyEnforcementFunctionMiddleware.process, a detected violation is resolved by this ladder (security.py, core 1.19.0):
binding: _PendingPolicyApproval | None = None
approved = False
if self.approval_on_violation:
try:
binding = self._pending_record(context, violations)
except (TypeError, ValueError, OverflowError):
self._block_unsafe_approval_binding(context, context_label=context_label)
approved = self._matches_pending_approval(context, binding)
if approved:
...
elif binding is not None:
self._request_policy_violation_approval(...)
elif self.block_on_violation:
self._block_policy_violation(...)
else:
logger.warning("WARNING: Tool '%s' policy violation(s) [%s] (allowed)", function_name, disclosed)
await call_next()
When _pending_record raises, _block_unsafe_approval_binding does not terminate the invocation, so control falls through with binding is None. approved is False, the elif binding is not None arm is skipped, and block_on_violation is False — forced so by the constructor, self.block_on_violation = block_on_violation if not approval_on_violation else False (line 2421). Execution therefore reaches the final else, logs "(allowed)", and runs await call_next(). The violating call executes.
The same violation with approval_on_violation=False is blocked correctly. So the safer-looking configuration — ask the human instead of refusing outright — is the one that can silently allow.
Reproduction
New in 1.19.0 (PR #8238, "Tighten security label enforcement"), a USER_IDENTITY label must carry a canonical principal set under agent_framework.security.principals. A tool that declares confidentiality: "user_identity" the pre-1.19 way produces a label without it; _canonical_principals then raises ValueError, which is exactly the exception the ladder above swallows.
- A tool declaring
{"source_integrity": "trusted", "confidentiality": "user_identity"} and no principals runs, tainting the context to USER_IDENTITY.
- A second tool declaring
max_allowed_confidentiality: "private" is called.
- Enforcer configured with
approval_on_violation=True.
Observed on core 1.19.0, with the violation correctly detected and logged:
WARNING agent_framework.security: Invalid USER_IDENTITY source principals for tool 'reads_user_identity': tool reads_user_identity principals must be a non-empty sequence
WARNING agent_framework.security: Policy violation detected
VIOLATION: {'type': 'confidentiality_violation', 'subtype': 'max_allowed_confidentiality',
'function': 'microsoft_docs_fetch',
'reason': 'Cannot write USER_IDENTITY data to PRIVATE destination (data exfiltration blocked)'}
…and the call then completing normally, returning a real result list. On core 1.18.0 the same sequence raised an approval request.
Two defects, separable
- The fall-through is the real bug. Whatever the reason an approval binding cannot be built, "we could not ask the user" must not degrade to "so we allowed it". The
except arm should block (or re-raise) rather than continue, independently of block_on_violation — which the caller cannot even set while approval is on.
- The trigger is a silent contract change. A
USER_IDENTITY label without principals is no longer a label that enforces; _read_source_principals logs a warning and returns {}. Anything that declared confidentiality: "user_identity" before 1.19.0 loses enforcement on upgrade, and the only signal is a WARNING line.
Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.19.0
Python Version
No response
Additional Context
Suggested fix
In the except (TypeError, ValueError, OverflowError) arm, treat the failure as a block: call _block_policy_violation(...) (or have _block_unsafe_approval_binding raise) so the ladder cannot reach the permissive else. A defence that cannot obtain a decision should refuse, not proceed.
Description
In
PolicyEnforcementFunctionMiddleware.process, a detected violation is resolved by this ladder (security.py, core 1.19.0):When
_pending_recordraises,_block_unsafe_approval_bindingdoes not terminate the invocation, so control falls through withbinding is None.approvedisFalse, theelif binding is not Nonearm is skipped, andblock_on_violationisFalse— forced so by the constructor,self.block_on_violation = block_on_violation if not approval_on_violation else False(line 2421). Execution therefore reaches the finalelse, logs "(allowed)", and runsawait call_next(). The violating call executes.The same violation with
approval_on_violation=Falseis blocked correctly. So the safer-looking configuration — ask the human instead of refusing outright — is the one that can silently allow.Reproduction
New in 1.19.0 (PR #8238, "Tighten security label enforcement"), a
USER_IDENTITYlabel must carry a canonical principal set underagent_framework.security.principals. A tool that declaresconfidentiality: "user_identity"the pre-1.19 way produces a label without it;_canonical_principalsthen raisesValueError, which is exactly the exception the ladder above swallows.{"source_integrity": "trusted", "confidentiality": "user_identity"}and no principals runs, tainting the context toUSER_IDENTITY.max_allowed_confidentiality: "private"is called.approval_on_violation=True.Observed on core 1.19.0, with the violation correctly detected and logged:
…and the call then completing normally, returning a real result list. On core 1.18.0 the same sequence raised an approval request.
Two defects, separable
exceptarm should block (or re-raise) rather than continue, independently ofblock_on_violation— which the caller cannot even set while approval is on.USER_IDENTITYlabel without principals is no longer a label that enforces;_read_source_principalslogs a warning and returns{}. Anything that declaredconfidentiality: "user_identity"before 1.19.0 loses enforcement on upgrade, and the only signal is a WARNING line.Code Sample
Error Messages / Stack Traces
Package Versions
agent-framework-core: 1.19.0
Python Version
No response
Additional Context
Suggested fix
In the
except (TypeError, ValueError, OverflowError)arm, treat the failure as a block: call_block_policy_violation(...)(or have_block_unsafe_approval_bindingraise) so the ladder cannot reach the permissiveelse. A defence that cannot obtain a decision should refuse, not proceed.