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.
handleris not called, and the model gets aToolMessagesaying the call was refused and which rule refused it. - Once stays once. The effect is reserved before
handlerruns and committed from its return, so two agents sharing a store cannot both execute the same effect key. - An unknown outcome stays unknown. Anything
handlerraises that is notNotExecutedleaves the effectAMBIGUOUS, 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 approveand 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.
Metadata
Release files for ctrlrun-langchain 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ctrlrun_langchain-1.0.0.tar.gz | 6.2 kB | With provenance | Details for ctrlrun_langchain-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ctrlrun_langchain-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for ctrlrun_langchain-1.0.0-py3-none-any.whl |
Total release size: 13.1 kB
Release files / ctrlrun_langchain-1.0.0.tar.gz
| Download URL | ctrlrun_langchain-1.0.0.tar.gz |
|---|---|
| Size | 6.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c07505ead64dd5596895fac7f12fd6a0f3e5575b7300cf8f0febf989b651fcc9
|
|
BLAKE2b-256 checksum How to use checksums |
4fd757ecd54d6e9d82c499bc324cff69a8b0b11e50e57b55c4628f5c3281517d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logRelease files / ctrlrun_langchain-1.0.0-py3-none-any.whl
| Download URL | ctrlrun_langchain-1.0.0-py3-none-any.whl |
|---|---|
| Size | 7.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b99b0a9671906974fd5c9f7be909b869da2c46383a262ee3dab50b406788ed06
|
|
BLAKE2b-256 checksum How to use checksums |
ad0783d3db47a46d43d013c4d5638e4343a4c009ada631008232796297af3c5a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logSecurity for ctrlrun-langchain 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ctrlrun_langchain-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.