Skip to main content

ctrlrun-langchain

Gate a LangChain agent's tool calls with a CTRLRun policy, through LangChain's own wrap_tool_call middleware hook.

  • Supported kernel range: ctrlrun>=0.12,<0.13
  • Supported framework range: langchain>=1.0,<2.0
  • Primitive reused: AgentMiddleware.wrap_tool_call, whose contract is "Intercept execution and control when the handler is called. You decide if the handler is called zero times (short-circuit), once (normal flow), or multiple times." Read 2026-09-16.
  • Framework shape: the framework hands over the call itself.

This is not the LangGraph adapter

ctrlrun-langgraph routes an APPROVE through interrupt(), reusing a human-in-the-loop primitive. This is a different thing on a different surface: LangChain's middleware gives the tool call itself to the middleware, so handler is the executor.

That closes the gap every observation-hook integration lives with. There is no separate outcome report to arrive late, be swallowed, or never fire. What the tool did is what handler returned or raised, in the same stack frame, and the receipt says so.

Three consequences, which are the reason to use this over a log-and-hope callback:

  • A denial never reaches the tool. handler is not called, and the model gets a ToolMessage saying the call was refused and which rule refused it.
  • Once stays once. The effect is reserved before handler runs and committed from its return, so two agents sharing a store cannot both execute the same effect key.
  • An unknown outcome stays unknown. Anything handler raises that is not NotExecuted leaves the effect AMBIGUOUS, and the next attempt is refused until a human resolves it, rather than being retried into a double charge.

You may not need this

@protect already covers any Python callable, including a LangChain tool, with no middleware and no framework support at all. This buys one thing over it: the gate applies to every tool the agent can reach, including tools you did not write and cannot decorate.

There is a third way in that is not an adapter at all: ctrlrun gateway puts the same guarantees in front of an MCP tool server, in any language, with no agent change.

Install

$ pip install ctrlrun-langchain

Use

The operator wires it, on the line where the policy, the store and the identity provider are chosen. This middleware never constructs a Control (SPEC-v0.5 §2.3), so everything it must not decide — the identity provider, the authority document, the environment, the mode — is chosen by the person deploying it.

from langchain.agents import create_agent
from ctrlrun import Control
from ctrlrun_langchain import CTRLRunMiddleware

control = Control.from_file("ctrlrun.yaml")

agent = create_agent(
    model="gpt-5.5",
    tools=[lookup, issue_refund],
    middleware=[CTRLRunMiddleware(control)],
)

With a policy that says refunds up to €50 are autonomous and the rest are denied:

schema: ctrlrun.policy/v2
actions:
  lookup:
    decision: allow
  issue_refund:
    effect: "refund:{payment_id}"
    rules:
      - when: { amount_gte: 0, amount_lte: 5000 }
        decision: allow
      - decision: deny

the agent's own tool calls are decided before they run:

lookup                              the tool runs
issue_refund  amount=900000         CTRLRun refused this call: rule[1]. The tool did not run.
rm_rf                               CTRLRun refused this call: unknown_action. The tool did not run.
issue_refund  amount=1000           the tool runs
issue_refund  amount=1000  (again)  CTRLRun refused this call: this effect is already committed

Nothing is default-allow: a tool the policy does not name is refused, which is why rm_rf above never reaches handler.

Every protected call needs a principal. In production that is an identity provider that verifies a credential; in development it is with ctrlrun.context(agent="support-agent"): around the agent invocation. Without one the action is denied before the policy is consulted:

ActionDenied: lookup: no principal is available; wrap the call in
'with ctrlrun.context(agent=...)', or install an identity provider that answers

That is fail-closed and deliberate: who is acting is an authorization input, and a library that accepted a self-asserted principal would be accepting the agent's word for its own authority.

Approvals

Where the policy says approve, this middleware refuses the call and tells the model the request id, rather than blocking the agent while a human deliberates:

CTRLRun is holding this call for a human. Approve it with 'ctrlrun approve apr_...',
then ask again. The tool did not run.

If you want the human answered inside the run instead, that is what ctrlrun-langgraph is for: LangGraph's interrupt() suspends the graph, and the resumed run re-presents the same proposal under the granted approval.

What this does not do

  • It does not decide anything. The policy does, and the policy is the operator's file.
  • It does not grant approvals. InterruptApprovalProvider, ctrlrun approve and the webhook are the only places a grant is written, and this is not one of them.
  • It does not supply a principal, and it never reads one from agent state.

Apache-2.0, same as the kernel.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ctrlrun_langchain-1.0.0.tar.gz (6.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ctrlrun_langchain-1.0.0-py3-none-any.whl (7.0 kB view details)

Uploaded Python 3

File details

Details for the file ctrlrun_langchain-1.0.0.tar.gz.

File metadata

  • Download URL: ctrlrun_langchain-1.0.0.tar.gz
  • Upload date:
  • Size: 6.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for ctrlrun_langchain-1.0.0.tar.gz
Algorithm Hash digest
SHA256 c07505ead64dd5596895fac7f12fd6a0f3e5575b7300cf8f0febf989b651fcc9
MD5 0de16164f77b029a01edef7ab2916c73
BLAKE2b-256 4fd757ecd54d6e9d82c499bc324cff69a8b0b11e50e57b55c4628f5c3281517d

See more details on using hashes here.

Provenance

The following attestation bundles were made for ctrlrun_langchain-1.0.0.tar.gz:

Publisher: publish.yml on CTRLRun/ctrlrun

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ctrlrun_langchain-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for ctrlrun_langchain-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b99b0a9671906974fd5c9f7be909b869da2c46383a262ee3dab50b406788ed06
MD5 1c9ad445970ed1ce895660d6690b2eea
BLAKE2b-256 ad0783d3db47a46d43d013c4d5638e4343a4c009ada631008232796297af3c5a

See more details on using hashes here.

Provenance

The following attestation bundles were made for ctrlrun_langchain-1.0.0-py3-none-any.whl:

Publisher: publish.yml on CTRLRun/ctrlrun

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page