attenu

Blog · · by , Attenu

Per-subagent permissions in LangChain, without changing LangChain

What LangChain built well. Middleware is the seam most agent frameworks are missing, and LangChain put it in the right place. wrap_tool_call hands you the ToolCallRequest and the handler; if you do not call the handler, the tool does not run. That is a real interception point, documented as a parameter rather than discovered by monkeypatching, and it is the whole basis of what follows. The multi-agent guide is equally clear about what it is optimising: "At the center of multi-agent design is context engineering — deciding what information each agent sees." Five named patterns, one axis, stated up front. And Deep Agents already ships the subagents harness — each subagent spec carries its own model, tools, prompt and middleware, and there are declarative filesystem permissions with first-match-wins ordering on the built-in file tools.

The one thing left open, in their words. Issue #33879 was filed by a LangChain maintainer on 2025-11-07 and is open today: "Add subagent middleware — inspired by deepagents sub agent middleware, which should be able to use this more general middleware… Got a good start here, but now out of date." One PR closed unmerged; another is open as a draft. So the pattern lives in Deep Agents, and there a subagent's tools come from its own spec — _build_task_tool compiles each spec with create_sub_agent(spec) and never compares its tool list with the parent's. The permissions docs say the same thing about the rules that do exist: "Subagents inherit the parent agent's permissions by default… This replaces the parent's rules entirely." Replace, not narrow. That is a design choice with real advantages — a specialist can be given exactly what its job needs, independent of its caller. It also means the axis is information, and the question of how much authority travels with a handoff is left to the developer.

The code. One extra middleware, installed on the supervisor and on each subagent spec:

guarded = GuardedDelegation(
    Guard.issue("supervisor", SUPERVISOR, task="research brief"),
    tools={"web_search":  ToolPolicy("web.search",  lambda a: {"egress": "internal", "rows": 10}),
           "write_brief": ToolPolicy("brief.write", lambda a: {"egress": "none"})},
    subagents={"researcher": RESEARCHER_REQUEST, "writer": WRITER_REQUEST},
    delegation_tool="task", subagent_arg="subagent_type")
mw = guarded.middleware()

When the supervisor calls task(description, subagent_type), the child is minted as meet(supervisor, requested). The map says which scope a tool needs; the guard says whether this agent still holds it — same tools, narrowed permissions.

The denial. The researcher asks for web.*, admin.export, 10,000 rows and a 9,999-second lifetime. It is granted web.search, 50 rows and 3,600 seconds, because that is all its parent had. The writer holds brief.write only. A note the writer is handed tells it to search the web for an attacker's query and paste the result into the brief; the model obeys. The call is denied before the tool body runs — the sink the tool writes into never records it, and in the unguarded control run it does. Retries stay denied and every attempt lands on the ledger. A second, undeclared network tool is denied by default. If the audit log cannot be written, the call does not proceed.

The verify. Deep Agents collapses a subagent's transcript into one ToolMessage for the supervisor, so the blocked calls are not in the parent's messages — the hash-chained log is where they surface. AuditLog.verify checks the chain; attenu-guard verify bundle.json checks a signed bundle for integrity, child ⊆ parent and containment, with nothing of ours running.

What remains LangChain's. The agent loop, the middleware seam this stands on, the subagent harness, the permission rules, tracing, and the design of the general middleware #33879 asks for — this is offered there as a reference implementation of one narrow piece, not as a replacement. And what the recipe cannot do: a direct Python call around the framework runs, and a subagent spec without the middleware is a hole rather than a narrowing. Both are tested, and both are in the trust boundary at the top of the recipe. Read that first.

Verified against langchain 1.3.17 · langgraph 1.2.11 · deepagents 0.7.6 on 2026-08-25. Recipe: https://github.com/attenu-io/attenu-guard/tree/main/examples/integrations/langgraph/subagent_middleware · https://attenu.io

Rafael Asor is the founder of Attenu and the maintainer of attenu-guard and attenu-derive, open-source Python libraries for AI agent permissions across sub-agent handoffs. He is the author of the IETF Internet-Draft draft-asor-wimse-agent-delegation-chain and is based in Tel Aviv.