attenu

Docs › attenu-guard › Integrations source: docs/INTEGRATIONS.md · updated · markdown

Integrations — attenu-guard inside real agent frameworks

Every row below is a shipped, tested integration: a thin adapter installed with the package as attenu_guard.adapters.<framework> (enable its framework with pip install 'attenu-guard[<extra>]'; the core stays zero-dependency — a framework is imported only when you import its adapter), a runnable demo.py under examples/integrations/<framework>/ that tells the poisoned-summariser story end to end, and a pytest under tests/integrations/ that runs offline — every framework's own mock/scripted model, no LLM API key — and is skipped when the framework isn't installed. CI runs each one against the exact version listed (integrations job) and, weekly, against the latest release (integrations-latest).

The library itself was not modified for any of them: Guard / Authority / Decision / AuditLog integrate through each framework's official hooks. Two hook points are always needed — (1) the moment a parent hands off to / spawns a sub-agent, where the child's attenuated Guard is minted with parent.delegate(...), and (2) the moment any agent invokes a tool, where guard.check(...) runs before the tool body.

Versions and file:line references are as of August 2026; they will drift.

The matrix

Framework (version tested) Delegation primitive Hook (1) — mint the child Guard Hook (2) — check before the tool body Offline model used in the test What the framework itself enforces about a sub-agent's authority Fit
LangGraph 1.2 + LangChain create_agent none in LangGraph itself (a sub-agent is another graph you call) construction site / the task tool call (deepagents) ToolNode(wrap_tool_call=…) / AgentMiddleware.wrap_tool_call (langgraph/prebuilt/tool_node.py, langchain/agents/middleware/types.py); the shipped guard_node/DelegatedToolNode for hand-written nodes scripted BaseChatModel (bind_tools passthrough) nothing; create_agent has no notion of parent/child 5
deepagents 0.7 (LangChain's multi-agent app) task(description, subagent_type) tool → subagent.invoke(...) (deepagents/middleware/subagents.py) the same wrap_tool_call, filtered on task wrap_tool_call in the sub-agent same SubAgent["tools"] (leaky: every sub-agent also inherits the filesystem suite); permissions on a sub-agent replace the parent's rules entirely (graph.py, verified: a child wrote /secrets/… where its parent was denied) 5
OpenAI Agents SDK 0.21 handoffs=[…], Agent.as_tool(...) RunHooks.on_handoff (agents/lifecycle.py; fires at turn_resolution.py) or handoff(..., on_handoff=…) FunctionTool.tool_input_guardrails (agents/tool.py), executed before on_tool_start and the body (tool_execution.py) agents.testing.ScriptedModel (shipped) nothing relative to a parent — tool lists are independent per agent; handoff forwards the entire conversation by default; RunHooks.on_tool_start cannot deny; a handoff cannot be vetoed 5
Google ADK 2.7 sub_agents=[…] + transfer_to_agent, AgentTool, mode='task' sub-agents BasePlugin.before_agent_callback (covers all three primitives; mode='task' never fires a tool callback) BasePlugin.before_tool_callback (google/adk/plugins/base_plugin.py; runs at flows/llm_flows/functions.py before the tool, a returned dict short-circuits) custom BaseLlm scripted per agent disallow_transfer_to_* shape the prompt + tool-schema enum only; the 2.x transfer path (workflow/utils/_transfer_utils.py) checks tree shape, not the flags; tool_filter is per-agent visibility, not parent-relative 5
Pydantic AI 2.31 documented "agent delegation" (a tool calling child.run(..., usage=ctx.usage)) — no framework primitive construction site: ctx.deps.delegate(...) inside the delegating tool AbstractCapability.before_tool_execute (pydantic_ai/capabilities/abstract.py; the only path to toolset.call_tooltool_manager.py) or WrapperToolset.call_tool FunctionModel with scripted ToolCallParts nothing delegation-aware; UsageLimits count/cost only; FilteredToolset/prepare = visibility 5
CrewAI 1.15 allow_delegation=TrueDelegate work to coworker tool; hierarchical manager the delegate tool call, inside the same before-tool hook crewai.hooks.register_before_tool_call_hook on both dispatch paths (utilities/tool_utils.py, agents/crew_agent_executor.py) — must abort with HookAborted: any other exception is swallowed and the tool runs (hooks/dispatch.py) BaseLLM subclass replaying ReAct / native tool-call text nothing: the coworker runs with its own full tool list (tools/agent_tools/base_agent_tools.py), selected by fuzzy role-name match on model output; guardrail= validates task output; events/callbacks are post-hoc 5
AutoGen (autogen-agentchat) 0.7 Swarm + Handoff, AgentTool/TeamTool Handoff.handoff_tool override (GuardedHandoff) — handoffs bypass the workbench (_assistant_agent.py) StaticStreamWorkbench.call_tool and call_tool_stream (the agent loop takes the stream branch) ReplayChatCompletionClient with FunctionCalls (needs ModelInfo(function_calling=True)) nothing: Handoff has target/description/name/message; receiver offers its own full tool list; intervention handlers cannot see tool calls 4
Microsoft Agent Framework 1.15 (the AutoGen + Semantic Kernel successor) Agent.as_tool(); handoff_to_<target> tools in handoff orchestration; workflow AgentExecutor participants the same function middleware, on the delegating tool — as_tool returns a plain FunctionTool (_agents.py:718) whose body starts the sub-agent (:694); handoff edges are handoff_to_<id> tools (agent_framework_orchestrations/_handoff.py:124) and their own _AutoHandoffMiddleware is appended (:269), so the guard runs first FunctionMiddleware.process — the body is reachable only through final_wrapper in FunctionMiddlewarePipeline.execute (_middleware.py:1126-1163); one registration covers the non-streaming (_tools.py:3174) and streaming (:3337) loops, which share one pipeline (:3628-3634) none shipped — a ScriptedChatClient composing FunctionInvocationLayer/ChatMiddlewareLayer over BaseChatClient, as the real providers do (agent_framework_openai/_chat_client.py:3430-3434) nothing parent-relative: a sub-agent keeps its own tool list and may hold more than its parent; approval_mode="always_require" is per-tool and static; security.py is information-flow labelling, not authority; middleware does not propagate into sub-agents — install the guard on every agent 5
AG2 1.0 (the AutoGen fork; package ag2, a rewrite) Agent.as_tool()task_<agent>; tasks=TaskConfig(...)run_subtask; background_agent_tool; the ag2.network delegate tool the same tool-execution hook, on the delegating tool — every handover in AG2 1.0 is a @tool (ag2/tools/subagents/subagent_tool.py:45-68, ag2/agent.py:1673-1706) BaseMiddleware.on_tool_execution (ag2/middleware/base.py:105), folded around the tool at ag2/tools/final/function_tool.py:120-124 and consumed at :127-128; plus per-tool ToolMiddleware (:97-104), which is the only hook that reaches a TaskConfig child ag2.testing.TestConfig replaying scripted ToolCallEvents (shipped) nothing parent-relative: TaskConfig.include_tools/exclude_tools filter inheritance and extra_tools can give a child more than its parent (ag2/agent.py:110-112, :1709-1730); recursion is blocked by tasks=False on the spawned child (:1468); agent middleware does not propagate into sub-agents (run_task.py:141,147; agent.py:1463-1469); ag2.network limits are hub-enforced (network/hub/arbiter.py:245-324) 5
Claude Agent SDK 0.2 subagents: ClaudeAgentOptions(agents={…: AgentDefinition}), invoked via the built-in Agent (né Task) tool SubagentStart hook (+ the parent's Agent PreToolUse) PreToolUse hook → permissionDecision: "deny"; agent_id on the tool event correlates the call to its subagent (the only framework here that ships that) none exists (the SDK drives the Claude Code CLI); tests drive the hook functions with the CLI's real payload shapes; live-verified on a logged-in Claude Code session (see the example README) real, code-enforced per-subagent tool allowlist (AgentDefinition.tools/disallowedTools) and absolute hook denies — but no argument-level ceilings, no child ⊆ parent relation, no revocation; can_use_tool is silently skipped for auto-approved tools; a parent in bypassPermissions overrides every subagent's mode 5
smolagents 1.26 managed_agents=[…] (a managed agent is duck-typed into a tool; MultiStepAgent.__call__) construction site (DelegatedAgent proxy in managed_agents) — no framework hook Tool.forward via a Tool subclass (GuardedTool) — one hook covers ToolCallingAgent and CodeAgent's sandbox; step_callbacks fire after the step scripted Model returning ChatMessageToolCalls nothing: managed agents keep their own tool list; only additional_authorized_imports (sandbox import allowlist, per-agent, not parent-relative) is code-enforced 4
AWS Strands 1.52 agents-as-tools (Agent.as_tool), Swarm (handoff_to_agent injected into every node), Graph BeforeToolCallEvent (agent-tool) / BeforeNodeCallEvent (swarm/graph) with cancel_node BeforeToolCallEventevent.cancel_tool = reason (checked before the executor runs the tool); also exposed as a Strands InterventionHandler custom Model emitting scripted tool-use stream events real, code-enforced per-agent tool registry, interventions (Deny/Confirm/Guide/Transform), Cedar policies — all per-agent/static; nothing relative to the caller; Swarm lets any node hand off to any node 5
LlamaIndex 0.14 AgentWorkflow(agents=[FunctionAgent(can_handoff_to=[…])]) with the injected handoff tool wrap the handoff tool in GuardedAgentWorkflow.get_tools(); a refused delegation clears next_agent guarded_tool(fn, scope=…) — a FunctionTool wrapper receiving the live Context; raises before the body; _call_tool turns it into ToolOutput(is_error=True) MockFunctionCallingLLM with scripted ToolCallBlocks can_handoff_to restricts routing, not authority; the target runs with its own tool list 4
Semantic Kernel 1.36 HandoffOrchestration (Handoff-transfer_to_<Target> functions per edge), agent-as-plugin AUTO_FUNCTION_INVOCATION filter on the transfer function (SK's own handoff idiom) FUNCTION_INVOCATION filter — not awaiting next(context) provably stops the body; covers auto tool-calling and direct kernel.invoke scripted ChatCompletionClientBase with FunctionCallContents nothing parent-relative; note Kernel.clone() deep-copies plugins and filters (filters must be closures; state must not live in plugins) 4
Agno 2.9 Team(members=[…]) — leader delegates via a generated delegate_task_to_member tool (hands over a task string) Team(tool_hooks=[…]) on the delegate function Agent(tool_hooks=[…]) — a hook that never calls function_call(**args) prevents the body (Agno sanitizes injected args before hooks "so a hook used as an authorization gate" is sound); pre_hooks are input guardrails, not tool gates scripted Model returning tool calls nothing: members keep their own tools, may hold more than the leader; leader tool_hooks don't propagate to members 5
Haystack (deepset haystack-ai) 3.1 AgentTool — a ComponentTool wrapping a whole Agent (haystack/tools/agent_tool.py) the AgentTool call itself: Haystack has no separate delegation callback, so the delegation moment is a tool invocation; the child Guard rides a ContextVar for the sub-run (a turn's parallel calls each get their own copy_context(), so a fan-out is siblings, not a chain) Tool.invoke / Tool.invoke_async via a subclass of the tool's own class (tools/tool.py; the only paths out of the run loop — components/agents/tool_calling.py), keeping isinstance(tool, ComponentTool) and the inputs_from_state/outputs_to_string machinery intact; alternatively a ConfirmationStrategy under ConfirmationHook at the before_tool hook point (hooks/human_in_the_loop/hooks.py, run before _run_tool in agent.py) a scripted ChatGenerator component replaying ToolCalls nothing: a sub-agent behind an AgentTool keeps its own tool list and may hold tools its caller lacks (pinned as test_haystack_itself_does_not_attenuate_a_sub_agent); the shipped ConfirmationHook is a per-tool human veto, not parent-relative 5
CAMEL-AI 0.2.90 AgentToolkit.agent_run_subagent (toolkits/agent_toolkit.py:286) — a persistent sub-agent per session; Workforce posts tasks to workers over a channel (societies/workforce/workforce.py:4071) the delegation call itself, via a GuardedAgentToolkit subclass — no framework hook fires at handoff FunctionTool subclass overriding both __call__ (toolkits/function_tool.py:613) and async_call (:700), the two ends of every path: ChatAgent._execute_tool tool(**args) (chat_agent.py:4048), _aexecute_tool's tool.func.async_calltool.async_call ladder (:4093-4099), and the streaming twins (:5031, :5165-5172) scripted BaseModelBackend returning ChatCompletions with tool calls nothing: _create_subagent (agent_toolkit.py:161) builds the child from ChatAgent._clone_tools() (chat_agent.py:6183) — a copy of the parent's whole toolset, agent_run_subagent included, so the child can delegate onward with everything too; a Workforce worker's tools are fixed at construction, not per assignment 4
A2A (Agent2Agent protocol) — a2a-sdk 1.1.2 the HOP itself: message:send to a remote agent in another process (A2A has no in-process sub-agent primitive) client side — ClientCallInterceptor.before (a2a/client/interceptors.py:46, run by BaseClient._intercept_before base_client.py:460) mints the child with parent.delegate(...) and puts the signed Delegation Chain on the message as an A2A extension (Message.extensions + Message.metadata[<uri>], spec §4.6.2, plus the A2A-Extensions header §4.6.1) server side — GuardedAgentExecutor wraps AgentExecutor.execute (a2a/server/agent_execution/agent_executor.py:15), verifies the chain offline (wire.load) and mints the served Guard from the leaf; guarded_tool(fn, scope=…) checks before each tool body via a ContextVar no model needed: the remote agent's plan is scripted; both halves run over InProcessTransport, an implementation of the SDK's public ClientTransport ABC — and over real HTTP in live_smoke.py (Starlette + uvicorn, verified) nothing: §7.6.4 says so explicitly — "the A2A protocol does not define the scope, representation, validity, or revocation semantics of the authorization decision or credential"; §7.6.3 notes in-band credentials are exposed to every agent in a chain. Per-hop authentication and the Agent Card are real and this stands on them 5

Fit = how well the framework's official hooks carry an authorization decision (1–5). Seventeen entries, seventeen offline test suites; the Claude Agent SDK integration was additionally verified live against a real session, and the A2A one over a real HTTP hop.

Beyond the matrix: a Langflow custom component (examples/integrations/langflow/, 25 offline tests) — Langflow is a visual builder, so the unit there is a component in the editor rather than an adapter module. See the section below.

Why these seventeen

Selection criteria (August 2026): (1) a Python framework with an explicit delegation / handoff / sub-agent primitive — the moment attenu-guard exists to guard; (2) coverage of every major vendor's agent stack (OpenAI, Google, Microsoft ×2, AWS, Anthropic, Hugging Face, LangChain) plus the leading independents (CrewAI, Pydantic AI, LlamaIndex, Agno); (3) an offline test path (mock/scripted model) so the integration test can run in CI with no API key; (4) at least one real multi-agent application, not just a framework (deepagents). GitHub stars at selection: CrewAI 57k · AutoGen 60k (in maintenance since 2026-04, superseded by Microsoft Agent Framework) · LlamaIndex 52k · Agno 42k · LangGraph 40k · smolagents 29k · OpenAI Agents SDK 29k · Semantic Kernel 28k · deepagents 28k · Haystack 26k · ADK 21k · Pydantic AI 19k · Claude Agent SDK 8k · Strands 7k. Added later (August 2026): Haystack (3.x shipped AgentTool, a delegation primitive), CAMEL-AI (AgentToolkit/Workforce proved hookable), and both AutoGen successors — Microsoft Agent Framework (AutoGen + Semantic Kernel) and AG2 1.0 (the AutoGen fork; a rewrite whose package is ag2, not autogen); AutoGen itself has been in maintenance since 2026-04, so its adapter now has two live successors beside it. Added after those: A2A — a protocol rather than a framework, but its hop IS a delegation, and spec §7.6.4 states outright that the protocol defines no scope or revocation semantics for an in-task authorization decision, so the adapter supplies them through A2A's own extension mechanism (§4.6). MCP remains a recipe rather than an adapter (below), because there the server is the resource, not a delegate. Deliberately not (yet): MetaGPT/ChatDev/AutoGPT (research/app-shaped, weak offline story), Letta (no delegation primitive), Dify/Flowise/n8n (not Python-embeddable), and every non-Python stack (see "Other languages").

Also added (August 2026): a Langflow custom component (below — Langflow is a visual builder, so the unit is a component in the editor rather than an adapter module).

Status of the framework findings

Nothing above has been reported upstream yet (this repository is private). Each finding is pinned as an executable baseline test in tests/integrations/ (e.g. test_autogen_itself_does_not_attenuate, deepagents' permissions-replace test), so the weekly unpinned integrations-latest job will flag the day a framework changes the behaviour — e.g. Google ADK's disallow_transfer_to_peers: upstream fixed #3850 on the legacy llm_flows path (fa18d26a, in 2.7.1), but the 2.x default workflow path (workflow/utils/_transfer_utils.py) still has no check — the pinned test asserts the transfer goes through on 2.7.1 and carries the message that will fire the day it stops. When the project goes public the intent is to file each as a constructive upstream issue with the repro and a suggested fix (child ⊆ parent "meet" semantics; fail-closed hook dispatch), and to keep this document as the citation.

Langflow — the visual builder

Langflow composes flows in a browser rather than in Python, so the unit of integration is a custom component you drop into the editor, not an adapter module: examples/integrations/langflow/attenu_guard_component.py.

Langflow tools are LangChain BaseTools, so hook (2) is the same funnel as the LangChain adapter's: BaseTool.invoke -> run -> _run, wrapped with a langchain_core.tools.StructuredTool that authorizes and only then calls inner.invoke(...), mirroring name / description / args_schema. Hook (1) is an edge in the flow: the component exposes its Guard as an output and accepts one on a Parent Authority input, and connecting them mints the downstream agent's authority with parent.delegate(...). Two chained components is a delegation whose child can only be narrower, drawn rather than coded.

The component also emits an Evidence output — the delegation graph, the audit log, and the result of re-verifying it — so a flow's authorization history is inspectable from inside the editor. Tested against lfx 1.11.5 / langchain-core 1.5; the parsing and wrapping half of the test suite runs with Langflow absent.

Other languages

The wire format (attenu_guard.wire: signed JWS Delegation Tokens, offline child ⊆ parent verification) is the language-neutral contract, and tests/vectors/ + scenarios/*.json are its conformance suite. A TypeScript port of the core is the natural next language (Vercel AI SDK, LangGraph.js, OpenAI Agents JS, Claude Agent SDK TS, the MCP TS SDK); Go/Java/.NET follow the same recipe: port the core against the vectors, then thin adapters at the same two hook points. Authority crosses a language boundary as a token, never as a network call on the deny path.

Denial semantics — one decision every adapter makes

Guard.check() returns a Decision; the adapter decides what the framework sees:

  • Return the denial to the model as a failed tool result (default in most adapters: ToolMessage(status="error"), ToolFailed, reject_content, ToolResult(is_error=True), ADK's error dict): the run continues, the model is told why and can recover. The tool body still never runs.
  • Raise (AuthorityDenied or the framework's own tripwire): the run aborts. Use for hard-stop policies. Every adapter exposes this as a one-word switch (on_deny="raise").

Either way the denial lands on the hash-chained audit log with a reason code, and attenu-guard view renders it in the delegation tree — that is the only place a sub-agent's blocked call surfaces in frameworks (deepagents, AutoGen) that collapse the child's transcript into a single message for the parent.

What we learned that shaped the library (v0.3)

  • strict_metering=True only refused an entirely empty context; a partial context that forgot one metered dimension silently skipped that ceiling. Now checked per ceiling (ctx_field_of, is_metered in ceilings.py).
  • Adapters need to refuse things upstream of policy (an agent the chain never delegated to, an unmapped tool). ReasonCode.NO_AUTHORITY + Guard.record_denial(...) put those on the same audit trail.
  • Guard.agent_id, Guard.is_revoked, Guard.is_expired — read-only state every registry and UI wanted.
  • revoke() was node-scoped: a framework that re-hands-off to a revoked agent (swarm ping-pong, a second as_tool() call) minted it a fresh, clean child from the still-valid parent; two adapters had to keep their own "revoked names" set. Guard.revoke_agent(agent_id) revokes the principal chain-wide (every node it holds, plus a grow-only ban that makes any later delegate() to it fail with agent_banned) in one auditable event.
  • Guard.would_delegate(agent_id, request) — dry-run of the delegation preconditions (revoked/expired parent, banned agent, depth/fanout) with no node created and no audit write, so a hook can pre-flight a handoff before it cancels a whole swarm.
  • The tool→scope map is the integrator's job and the only real work (~10 minutes once you know your tools). Adapters default to fail-closed on an unmapped tool.
  • Frameworks run parallel tool calls on thread pools (smolagents, ADK): concurrent check()s could interleave the hash-chain append and make verify() reject the library's own log. The audit log, sequence clock and chain mutations are now serialised per chain (ts and seq advance together).
  • Ceiling.describe() / Authority.describe() and ReasonCode.{CHAIN_REVOKED, AGENT_BANNED, TTL_EXPIRED, MAX_DEPTH, MAX_FANOUT, CHAIN_CEILING} — every adapter demo had re-invented the first; every adapter had a lookup table for the second.

Running one

pip install -e '.[openai-agents]'             # extras: langchain, deepagents, openai-agents, google-adk,
                                              # pydantic-ai, crewai, autogen, claude-agent-sdk, smolagents,
                                              # strands, llama-index, semantic-kernel, agno
python examples/integrations/openai_agents/demo.py
python -m pytest -q tests/integrations/test_openai_agents.py
from attenu_guard.adapters.openai_agents import GuardRegistry, DelegationGuardHooks, guarded_tool

Each directory's README.md lists the exact hooks, the version tested, and an env-gated live_smoke.py (RUN_LIVE=1 + your provider key) that replays the same story against a real model.