[AAASM-3107] 🔒 (adapters): Fail closed on unknown/None decision under enforce#139
Conversation
_normalize_decision mapped unknown/None/malformed verdicts to allow, so a denied action could slip through the SDK layer. Under enforce (enforcement_mode == "enforce", threaded in by AAASM-3106) such verdicts now deny; observe/disabled still fail open. Shared by the MCP adapter. Refs AAASM-3107
AssemblyCallbackHandler._normalize_decision defaulted unknown/None/ malformed verdicts to allow. Under enforce it now denies (reusing the interceptor's _enforce flag from AAASM-3106); observe/disabled fail open. Refs AAASM-3107
Thread the enforce posture into the MCP call_tool patch so an unknown/ None/malformed verdict denies under enforce instead of allowing, forwarding it to the shared crewai _normalize_decision. Refs AAASM-3107
Refs AAASM-3107
Sync and async on_tool_start deny unknown/None/malformed verdicts under enforce and pass them through otherwise. Refs AAASM-3107
Refs AAASM-3107
Compare _enforce strictly against True so an interceptor whose __getattr__ synthesizes truthy values for missing attributes is not mistaken for the enforce posture. The real flag is always a bool on RuntimeQueryInterceptor / _FailClosedInterceptor. Refs AAASM-3107
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
🔎 Claude Code review — Wave 2 security remediationCI: All checks green (codecov/Sonar advisory, out of scope). |



Description
_normalize_decisionin the LangChain, CrewAI, and MCP adapters mapped anyunknown /
None/ malformed governance verdict to allow, so a deniedaction could slip through the SDK interception layer. This PR makes those
helpers fail closed when the agent is in
enforceposture: an unrecognized,None, or malformed verdict now maps to deny. Underobserve/disabled(and when no native runtime authority is engaged) the helpers still fail open,
preserving the dry-run / hermetic behavior.
The fix reuses the
enforcement_modeplumbing landed in Wave-1 (AAASM-3106 /#138): the governance interceptor (
RuntimeQueryInterceptor/_FailClosedInterceptor) already carries an_enforcebool derived fromenforcement_mode == "enforce". Each adapter reads that flag (strictly, viais True) and threads it into_normalize_decision.Type of Change
Breaking Changes
Behavior only tightens under
enforcefor verdicts that were never valid(unknown /
None/ malformed). Validallow/deny/pendingverdicts andall
observe/disabledbehavior are unchanged.Related Issues
Testing
Added regression tests for each adapter asserting unknown/
None/malformedverdicts deny under
enforceand pass through otherwise (LangChain sync +async, CrewAI, MCP). Full suite: 513 passed, 13 skipped.
mypyclean.Checklist
Closes AAASM-3107
🤖 Generated with Claude Code