Skip to content

[AAASM-4014] 🐛 (sdk): Thread real interceptor through LangChain callback handler + fail-closed unknown#204

Merged
Chisanan232 merged 4 commits into
masterfrom
v0.0.1/AAASM-4014/fix/langchain_coinstall_bypass
Jul 3, 2026
Merged

[AAASM-4014] 🐛 (sdk): Thread real interceptor through LangChain callback handler + fail-closed unknown#204
Chisanan232 merged 4 commits into
masterfrom
v0.0.1/AAASM-4014/fix/langchain_coinstall_bypass

Conversation

@Chisanan232

Copy link
Copy Markdown
Contributor

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.

  • Primary fix (adapters/langchain/callback_handler.py): AssemblyCallbackHandler now delegates missing attributes to its wrapped real interceptor via __getattr__. This routes check_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.
  • Fail-closed unknown decision (core/runtime_interceptor.py): RuntimeQueryInterceptor.check_tool_start no longer defaults a missing/empty/unknown native decision to allow. Only allow/redact/unspecified are treated as authoritative allow; anything else routes through _on_query_failure (deny under enforce, proceed under observe).
  • Defense-in-depth (crewai, pydantic_ai, llamaindex, haystack, google_adk, microsoft_agent_framework): the method-absent tool-check fallback now denies under enforce via a shared _missing_interceptor_decision helper instead of defaulting to {"status": "allow"}.

Why

When langchain is importable it registers first (registry priority 0) and core/assembly.py reassigns the governance interceptor to the AssemblyCallbackHandler, threading it to every subsequently-registered adapter. That handler exposed only the LangChain callback contract — no check_tool_start, no delegation — so adapters doing getattr(cbh, "check_tool_start", None) got None and 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_agents and mcp already unwrap _interceptor and were unaffected.

Separately, the runtime interceptor defaulted an unrecognized native verdict to allow regardless of _enforce, an additional fail-open under enforce.

Reproduction

With langchain co-installed and enforcement enforce, register a non-LangChain adapter (e.g. crewai). A tool the runtime denies was allowed to run because the crewai patch's check_tool_start lookup on the threaded AssemblyCallbackHandler returned None. New test test_langchain_coinstall_denies_through_crewai_adapter reproduces this and asserts the tool is now denied.

How verified

  • uv sync
  • Full suite: .venv/bin/python -m pytest test/740 passed, 15 skipped
  • .venv/bin/mypy agent_assembly --ignore-missing-imports → clean
  • Pre-commit gate (isort/autoflake/black/mypy) on changed files → all pass
  • New tests: co-install deny-through-crewai, unknown/empty/missing decision deny-under-enforce (+ allow-under-observe), missing-interceptor fallback, callback-handler delegation, and no-recursion AttributeError.

Note: the UDS stat-then-connect TOCTOU in connect_runtime_client is advisory/low — SO_PEERCRED in aa-sdk-client is the authoritative check; not changed here.

Closes AAASM-4014

🤖 Generated with Claude Code

https://claude.ai/code/session_01MvjnG3ysnqTY6Gu1wQ2h73

Chisanan232 and others added 4 commits July 2, 2026 22:23
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
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

@Chisanan232

Copy link
Copy Markdown
Contributor Author

🤖 Claude Code review — approve

AAASM-4014 LangChain co-install bypass. Delegating __getattr__ on AssemblyCallbackHandler forwards missing attrs (incl. check_tool_start) to the wrapped _interceptor (with recursion guard) → non-LangChain adapters reach real governance. runtime_interceptor unknown/empty/missing decision → deny under enforce; method-absent fallback denies under enforce across crewai/pydantic_ai/llamaindex/haystack/google_adk/msaf. 740 pass incl. reproduce-then-fix.

Note: codecov/patch red = coverage-delta acceptance gate only — ignorable per policy.

@Chisanan232

Copy link
Copy Markdown
Contributor Author

🤖 Claude Code — deep review: APPROVE (merge-ready)

CI: green except codecov/patch (acceptance-only). Scope: complete — delegating __getattr__ on AssemblyCallbackHandler (+ recursion guard) routes missing check_tool_start to the real _interceptor; runtime_interceptor unknown/empty/missing→deny under enforce; method-absent fallback denies under enforce across crewai/pydantic_ai/llamaindex/haystack/google_adk/msaf. openai_agents/mcp correctly untouched (already unwrap _interceptor).

Side-effects (regression check): the __getattr__ does NOT disturb the LangChain contract — BaseCallbackHandler defines every on_*/ignore_*/raise_error concretely on the class, so __getattr__ never fires for them (dispatch + ignore-flags unaffected); explicit callbacks shadow the base; enforce-detection uses is True so a synthesized-truthy stub can't fake enforce. Unknown→deny preserves observe-mode allow (tested). 740 pass, no prior flow expected allow. No regression.\n\nNon-blocking follow-up: langchain_core is NOT a test dep, so the __getattr__-vs-real-LangChain-base contract safety is argued, not CI-tested — add a langchain-installed test asserting ignore/dispatch resolve to the base and a co-installed crewai check denies end-to-end.

@Chisanan232 Chisanan232 merged commit 8f14660 into master Jul 3, 2026
24 of 25 checks passed
@Chisanan232 Chisanan232 deleted the v0.0.1/AAASM-4014/fix/langchain_coinstall_bypass branch July 3, 2026 00:30
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