[AAASM-4734] 🐛 (adapters): Fail closed on unrecognized verdict and missing interceptor - #269
Conversation
An unrecognized/malformed verdict routed through the local _normalize_decision dropped the enforce flag, so it fell open (allow) even under enforce. Give the wrapper an enforce param and thread _interceptor_enforces(callback_handler) through both call sites so an unknown verdict fails closed under enforce (AAASM-4734).
When the wired interceptor exposed no callable check_tool_start,
_invoke_async_tool_check hardcoded {"status": "allow"}, silently skipping
pre-execution governance. Route through _missing_interceptor_decision so it
denies under enforce, matching pydantic_ai/google_adk (AAASM-4734).
The local _normalize_decision dropped the enforce flag, so an unrecognized/ malformed verdict fell open (allow) even under enforce. Give the wrapper an enforce param and thread _interceptor_enforces(callback_handler) through all four call sites in both patched tool hooks (AAASM-4734).
The local _normalize_decision dropped the enforce flag, so an unrecognized/ malformed verdict fell open (allow) even under enforce. Give the wrapper an enforce param and thread _interceptor_enforces(callback_handler) through both call sites in patched_run_async (AAASM-4734).
When the wired interceptor exposed no callable check_tool_start,
_invoke_async_tool_check hardcoded {"status": "allow"}, silently skipping
pre-execution governance. Route through _missing_interceptor_decision so it
denies under enforce, matching pydantic_ai/google_adk (AAASM-4734).
_register_agent_with_gateway hard-failed init on a register() error only for a literal enforcement_mode == "enforce", so the advertised None default (which registers under the gateway's live enforce default) fell open. Reuse the None-aware _local_posture_is_enforce so an omitted posture propagates a register failure like enforce (AAASM-4734).
Regression for AAASM-4734: an enforcing interceptor returning a malformed (None) verdict must block the tool instead of falling open.
Regression for AAASM-4734: an enforcing handler exposing no check_tool_start must block the tool instead of the previous hardcoded allow.
Regression for AAASM-4734: an enforcing interceptor returning a malformed (None) verdict must raise instead of falling open.
Regression for AAASM-4734: an enforcing handler exposing no check_tool_start must raise via _missing_interceptor_decision rather than allow.
Regression for AAASM-4734: an enforcing interceptor returning a malformed (None) verdict must raise instead of falling open.
Regression for AAASM-4734: an enforcing handler exposing no check_tool_start must raise via _missing_interceptor_decision rather than allow.
Regression for AAASM-4734: an enforcing handler exposing no check_tool_start must raise MCPToolBlockedError rather than the previous hardcoded allow.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Claude Code — automated reviewCI: All green except SonarCloud Code Analysis (acceptance/quality-gate tier — ignorable per review policy). Full unit + integration + contract suites, CodeQL, and pip-audit all pass. Scope vs AAASM-4734: Complete. All four divergent adapters (openai_agents, pydantic_ai, google_adk, mcp) now thread Side effects: None on existing behavior — the already-correct sibling adapters are untouched, and the green full-suite CI confirms no regressions. The only behavior change is the intended one: unrecognized verdict / missing interceptor now DENY under enforce instead of silently allowing. Verdict: ✅ Ready for approval & merge (once Sonar gate is waived). |
SonarCloud flagged the four adapters' near-identical fail-closed regression tests as duplicated new lines (5.0% > 3%). Extract the two enforcing interceptor stand-ins and the (unknown-verdict, missing-check) parametrization into test/unit/adapters/enforce_helpers.py, and collapse each adapter's two copy-paste tests into one parametrized test_denies_under_enforce. Coverage is unchanged: openai_agents/pydantic_ai/google_adk still exercise both scenarios; mcp keeps its pre-existing malformed test plus the missing-interceptor case (AAASM-4734).
|
Claude Code — review update (SonarCloud)The remaining red is SonarCloud → new_duplicated_lines_density 5.7% > 3%. Investigated: the duplicated blocks are cross-file between Getting under 3% would require consolidating the two parallel adapters into a shared base — a large, higher-risk refactor of untouched code, out of scope for this security fix (scope discipline). The fix itself is correct and fully test-covered (78 adapter tests green; all other checks green). Decision (confirmed with maintainer): accept this as the acceptance-tier SonarCloud exception and merge with the gate waived. A separate tech-debt ticket will track de-duplicating the google_adk/pydantic_ai adapters at the root. Verdict: ✅ Ready to merge (SonarCloud duplication waived as a documented pre-existing-duplication exception). |


Description
Several framework adapters failed open under
enforce, silently allowing a toolcall that governance should have blocked. This PR makes them fail closed, mirroring
the already-correct sibling adapters (
agno,crewai,haystack,microsoft_agent_framework).Two distinct fail-open paths were fixed:
Unrecognized / malformed verdict dropped the enforce flag. The shared normalizer
crewai.patch._normalize_decision(decision, *, enforce=False)denies an unknown/None/malformed verdict only when
enforce=True. Three adapters wrapped it with a local_normalize_decision(decision)that droppedenforce, so an unknown verdict allowed evenunder enforce. Each local wrapper now takes
enforce: bool = Falseand every call sitethreads
_interceptor_enforces(callback_handler)through:openai_agents/patch.pypydantic_ai/patch.pygoogle_adk/patch.pyMissing
check_tool_starthardcoded allow. When a co-installed interceptor exposed nocallable
check_tool_start, the invoke helper returned a literal{"status": "allow"},skipping pre-execution governance. Replaced with
crewai.patch._missing_interceptor_decision(callback_handler)(denies under enforce),matching
pydantic_ai/google_adkwhich already did this:openai_agents/patch.pymcp/patch.pyCore register-failure posture (LOW, same theme).
core/assembly.py::_register_agent_with_gatewayhard-failed init on aregister()erroronly for a literal
enforcement_mode == "enforce", so the advertisedNonedefault (whichregisters under the gateway's live enforce default) fell open. It now reuses the None-aware
runtime_interceptor._local_posture_is_enforce, so an omitted (None) posture propagates aregister failure exactly like
enforce.Sibling adapters that were already correct were left untouched.
Type of Change
Breaking Changes
Behavior only changes under an enforce posture and only for previously-unhandled
(malformed / missing-interceptor) inputs, which now correctly deny instead of allow.
Observe/disabled postures still fail open (dry-run preserved); the
enforce=Falsedefault of every wrapper is unchanged.
Related Issues
Fixes AAASM-4734.
Testing
Describe the testing performed for this PR:
Added regression tests under
test/unit/adapters/{openai_agents,pydantic_ai,google_adk,mcp}/covering, per affected adapter:
None) verdict → DENY;check_tool_start→ DENY.(
mcpalready had the malformed-verdict-under-enforce test; its fix and new test cover themissing-interceptor path.) All 7 new tests pass locally.
Checklist
🤖 Generated with Claude Code
https://claude.ai/code/session_01HMnPRm9T3fdrS4uNYXkzg6