New to the adapters? Start with the wrapping overview and comparison
table.
Compatible versions: see the compatibility
table for the verified
langchain range. deepagents rides this adapter but has its own row and range
in that table — check the deepagents row, not the langchain one.wrap_langchain_agent builds a PolicyEnforcer once and installs it on each tool
in place (install_enforcer_on_tool) so the same instances inside the compiled
graph become policy-gated. It returns a HexgateLangchainAgent proxy that opens a
HexgateContext scope and injects a Langfuse callback into every invoke / ainvoke /
stream / astream / astream_events call. The hexgate_context is supplied per call,
so a single wrapped agent can serve many users concurrently — role resolution
happens at call time from the contextvar.
Under the hood
wrap_langchain_agentbuilds aPolicySetfor the agent, constructs onePolicyEnforcer(policy_set, agent_name=…), and callsinstall_enforcer_on_tools(tools, enforcer=…)to mutate each tool’sfuncandcoroutinewith enforcer-gated closures.handle_tool_erroris forced toTrue. Installation is idempotent — re-installing rebinds the captured originals to the new enforcer without stacking gates.- Each invocation method on
HexgateLangchainAgenttakeshexgate_context=and opens anasync with hexgate_context:(orhexgate_context.sync_scope()for sync) around the delegatedCompiledStateGraphcall. The active context is pushed onto a contextvar; the guards read it at tool-call time to resolve the matching role’s policy. - A non-allow
Decisionis rendered as{"ok": False, "error": decision.as_error_payload()}so the LangChain runtime surfaces the structured dict as the tool result instead of raising. - The wrapper also enters
propagate_attributes(...)and merges a LangfuseCallbackHandlerinto theRunnableConfig.callbacksfor the duration of the call. Anything not explicitly proxied falls through via__getattr__.
Because LangChain BYO-graph tools are mutated in place by design, the same
tools list you pass to create_react_agent flows through already gated — the
wrapper holds the policy.The policy is resolved by agent name, so pass
name="devops_agent" to
create_react_agent — it must match the agent you registered on the platform.
Omit it and the compiled graph keeps LangGraph’s default name ("LangGraph"),
so the wrapper resolves the policy for an agent called LangGraph and fails
with a 404 unless one is registered under that name.Agent-level enforcement
Agent-level policy (
admission / agents)
is not yet enforced on the LangChain adapter — a declared block logs a warning at
setup rather than gating. A sub-agent exposed as a tool is still gated normally by
its tool name. (The native create_agent orchestrator does enforce admission;
this adapter, which wraps a pre-built LangChain/LangGraph agent, is a planned
follow-up.)deepagents skills
deepagents rides this adapter. There is no separate deepagents adapter:create_deep_agent returns a CompiledStateGraph, and wrap_langchain_agent
wraps it like any other graph. Its skills are discovered and
gated from the graph’s own SkillsMiddleware:
wrap_langchain_agent(..., skills_middleware=...).
How a skill is recognised
deepagents has no “load skill” action. The system prompt lists each skill’sSKILL.md path, and the model activates one by calling read_file on it. When
the agent is wrapped, Hexgate indexes every known skill path; once the policy
lists a skill under skills:, a read_file that matches is decided under the skill’s
key instead of read_file’s:
Paths are normalised the way deepagents reads them, so
skills/x/SKILL.md and
\skills\x\SKILL.md both match /skills/x/SKILL.md, and args.file_path carries
that one spelling. The content_hash is read just before the file is, so a
pin checks the body as it stands at
decision time.
A denial uses the same wording as Google ADK
— it is shared across adapters — as the message of the usual
{"ok": False, "error": ...} payload.
Limitations
-
skill.script:is not supported here. A deepagents script runs throughexecute, and matching anexecutecall to a skill’s scripts would mean parsing shell strings.executeis gated on its own terms undertools:instead, on the top-level agent only (a sub-agent’sexecuteis not gated, see below); avia: [script]rule never fires on this adapter. -
Skills whose paths are not known at wrap time are not gated — a source
added mid-run, or a backend populated per invocation. Their reads decide as
ordinary
read_filecalls. -
Sub-agents are not gated. Hexgate gates only the tool objects it wraps:
those passed to
wrap_langchain_agent(tools=)and those bound on the top-level graph. deepagents’tasktool runs each sub-agent as a separate graph inside the tool call, and any tool a sub-agent runs that is not one of those objects runs with no decision. That includes:- the filesystem tools every sub-agent builds on the same backend (
ls,read_file,write_file,edit_file,glob,grep, andexecutewhen the backend can run commands), soexecute: denyor a denied skill’sSKILL.mdholds for the top-level agent only; - tools a sub-agent spec declares in its own
toolslist, unless the same objects are also passed towrap_langchain_agent(tools=); - everything inside a
CompiledSubAgent.
taskcall itself is recorded.taskis a top-level tool gated undertools:, so if a denial must hold inside sub-agents, denytasktoo, or leave it unlisted under a deny-default policy. That withdraws every sub-agent. Gating sub-agent tools is tracked in #273. - the filesystem tools every sub-agent builds on the same backend (
-
Not containment. A model can reach a skill’s text another way —
grep,lsplus a read of a variant spelling, a copy elsewhere on the backend. This gates the documented activation path on the top-level agent (not inside sub-agents, see above), which is what the audit trail and the kill-switch need. The tool layer remains the boundary. -
The pin is a separate read. The hash is taken just before
read_filereads the file itself, so on a writable backend a body changed between the two reads can slip past a pin.
Runnable example
examples/devops_langchain.py — wrap_langchain_agent (LangChain / LangGraph)
end-to-end.