Skip to content

[AAASM-4734] 🐛 (adapters): Fail closed on unrecognized verdict and missing interceptor - #269

Merged
Chisanan232 merged 14 commits into
masterfrom
v0.0.1/AAASM-4734/adapter_enforce_fail_open
Jul 17, 2026
Merged

[AAASM-4734] 🐛 (adapters): Fail closed on unrecognized verdict and missing interceptor#269
Chisanan232 merged 14 commits into
masterfrom
v0.0.1/AAASM-4734/adapter_enforce_fail_open

Conversation

@Chisanan232

@Chisanan232 Chisanan232 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Description

Several framework adapters failed open under enforce, silently allowing a tool
call 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:

  1. 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 dropped enforce, so an unknown verdict allowed even
    under enforce. Each local wrapper now takes enforce: bool = False and every call site
    threads _interceptor_enforces(callback_handler) through:

    • openai_agents/patch.py
    • pydantic_ai/patch.py
    • google_adk/patch.py
  2. Missing check_tool_start hardcoded allow. When a co-installed interceptor exposed no
    callable 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_adk which already did this:

    • openai_agents/patch.py
    • mcp/patch.py
  3. Core register-failure posture (LOW, same theme).
    core/assembly.py::_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. It now reuses the None-aware
    runtime_interceptor._local_posture_is_enforce, so an omitted (None) posture propagates a
    register failure exactly like enforce.

Sibling adapters that were already correct were left untouched.

Type of Change

  • 🔧 Bug fix

Breaking Changes

  • No

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=False
default of every wrapper is unchanged.

Related Issues

Fixes AAASM-4734.

Testing

Describe the testing performed for this PR:

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed

Added regression tests under test/unit/adapters/{openai_agents,pydantic_ai,google_adk,mcp}/
covering, per affected adapter:

  • an enforcing interceptor returning a malformed (None) verdict → DENY;
  • an enforcing handler exposing no check_tool_startDENY.

(mcp already had the malformed-verdict-under-enforce test; its fix and new test cover the
missing-interceptor path.) All 7 new tests pass locally.

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Comments added for complex logic
  • Documentation updated if needed
  • All tests passing

🤖 Generated with Claude Code

https://claude.ai/code/session_01HMnPRm9T3fdrS4uNYXkzg6

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

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
agent_assembly/adapters/pydantic_ai/patch.py 85.71% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@Chisanan232

Copy link
Copy Markdown
Contributor Author

Claude Code — automated review

CI: 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 enforce / use _missing_interceptor_decision, plus the core/assembly.py register-propagation None≡enforce fix. 7 regression tests added (malformed-verdict + missing-interceptor deny-under-enforce).

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).
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@Chisanan232

Copy link
Copy Markdown
Contributor Author

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 agent_assembly/adapters/google_adk/patch.py and agent_assembly/adapters/pydantic_ai/patch.py (up to ~126-line spans). Those two adapters are pre-existing near-identical copies; the security fix only threaded the enforce kwarg through a few call sites that happen to sit inside those already-duplicated bodies, so Sonar attributes the duplication to this diff.

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).

@Chisanan232
Chisanan232 merged commit 636c99d into master Jul 17, 2026
25 of 26 checks passed
@Chisanan232
Chisanan232 deleted the v0.0.1/AAASM-4734/adapter_enforce_fail_open branch July 17, 2026 04:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant