[AAASM-4014] 🐛 (sdk): Thread real interceptor through LangChain callback handler + fail-closed unknown#204
Conversation
When langchain is co-installed it registers first and its AssemblyCallbackHandler is threaded to every subsequently-registered adapter as the governance interceptor. Adapters look up check_tool_start (and approval/event entry points) directly on that handler, which the LangChain callback contract does not expose, so the lookup returned None and the adapter fell back to allow — bypassing pre-exec governance under enforce (AAASM-4014). Add a delegating __getattr__ that forwards missing attributes to the wrapped real interceptor (RuntimeQueryInterceptor / _FailClosedInterceptor), guarding _interceptor against recursion. Explicit callback methods still take precedence, so LangChain's own dispatch is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73
RuntimeQueryInterceptor.check_tool_start defaulted a missing/empty/unknown native decision to allow, ignoring _enforce. Under enforce that silently permitted any verdict the runtime did not label deny/pending/error. Allowlist only the authoritative-allow verdicts (allow/redact/unspecified) and route anything else — unknown string, empty, or missing decision key — through _on_query_failure, which denies under enforce and proceeds under observe (AAASM-4014). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73
…_start Defense-in-depth for AAASM-4014. The tool-check helpers in the non-LangChain adapters defaulted to allow when the wired interceptor exposed no check_tool_start. Add a shared _missing_interceptor_decision helper (crewai; local copy in the self-contained haystack) that fails closed under enforce and proceeds under observe, and route every method-absent fallback through it (crewai, pydantic_ai, llamaindex, haystack, google_adk, microsoft_agent_framework). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73
…cision Add reproduce-then-fix coverage for AAASM-4014: a LangChain-registered AssemblyCallbackHandler threaded to a non-LangChain (crewai) adapter now honors a runtime DENY under enforce; unknown/empty/missing native decisions deny under enforce and proceed under observe; the missing-interceptor fallback denies under enforce; and the callback handler delegates governance entry points to the wrapped interceptor while keeping its explicit LangChain callbacks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
🤖 Claude Code review — approve AAASM-4014 LangChain co-install bypass. Delegating Note: |
|
🤖 Claude Code — deep review: APPROVE (merge-ready) CI: green except Side-effects (regression check): the |



What
Fixes AAASM-4014 [MED]: a LangChain co-install silently bypassed pre-execution governance for non-LangChain adapters, plus a related fail-open on unknown native decisions.
adapters/langchain/callback_handler.py):AssemblyCallbackHandlernow delegates missing attributes to its wrapped real interceptor via__getattr__. This routescheck_tool_start, approval, timeout, and event entry points to genuine governance. Its explicit LangChain callback methods still take precedence, so LangChain's own dispatch is unaffected.core/runtime_interceptor.py):RuntimeQueryInterceptor.check_tool_startno longer defaults a missing/empty/unknown nativedecisionto allow. Onlyallow/redact/unspecifiedare treated as authoritative allow; anything else routes through_on_query_failure(deny under enforce, proceed under observe)._missing_interceptor_decisionhelper instead of defaulting to{"status": "allow"}.Why
When
langchainis importable it registers first (registry priority 0) andcore/assembly.pyreassigns the governanceinterceptorto theAssemblyCallbackHandler, threading it to every subsequently-registered adapter. That handler exposed only the LangChain callback contract — nocheck_tool_start, no delegation — so adapters doinggetattr(cbh, "check_tool_start", None)gotNoneand fell back to allow, skipping pre-exec governance under enforce. Affected adapters: crewai, pydantic_ai, llamaindex, haystack, google_adk (and microsoft_agent_framework, same pattern).openai_agentsandmcpalready unwrap_interceptorand were unaffected.Separately, the runtime interceptor defaulted an unrecognized native verdict to allow regardless of
_enforce, an additional fail-open under enforce.Reproduction
With
langchainco-installed and enforcementenforce, register a non-LangChain adapter (e.g. crewai). A tool the runtime denies was allowed to run because the crewai patch'scheck_tool_startlookup on the threadedAssemblyCallbackHandlerreturnedNone. New testtest_langchain_coinstall_denies_through_crewai_adapterreproduces this and asserts the tool is now denied.How verified
uv sync.venv/bin/python -m pytest test/→ 740 passed, 15 skipped.venv/bin/mypy agent_assembly --ignore-missing-imports→ cleanAttributeError.Note: the UDS stat-then-connect TOCTOU in
connect_runtime_clientis advisory/low —SO_PEERCREDinaa-sdk-clientis the authoritative check; not changed here.Closes AAASM-4014
🤖 Generated with Claude Code
https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73