From ad151cb04b8905b8d67dbeb0adafc603995a857c Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 16 Sep 2026 14:36:33 +0530 Subject: [PATCH 1/2] The LangChain middleware page, now that the package is on PyPI `ctrlrun-langchain` 1.0.0 shipped on 2026-09-15. The kernel repository carried a finished page for it, held until the package existed; this is that page, here, at `/docs/guides/langchain-middleware`, with a nav entry, a redirect, and a row in the adapters table saying what the adapter's own README says: the call itself is handed over through `AgentMiddleware.wrap_tool_call`, and the binding is prevention. Its LangChain links are absolute now that it lives on this site. It is the partner doc the LangChain integrations table will link to. Signed-off-by: arpan --- docs.json | 6 ++ docs/adapters.md | 1 + docs/guides/langchain-middleware.mdx | 109 +++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 docs/guides/langchain-middleware.mdx diff --git a/docs.json b/docs.json index aa438b0..ced1ba3 100644 --- a/docs.json +++ b/docs.json @@ -109,6 +109,7 @@ "docs/guides/verify-in-ci", "docs/guides/export-to-opentelemetry", "docs/adapters", + "docs/guides/langchain-middleware", "docs/guides/langgraph-adapter", "docs/guides/openai-agents-adapter" ] @@ -611,6 +612,11 @@ "destination": "/docs/guides/export-to-opentelemetry", "permanent": true }, + { + "source": "/guides/langchain-middleware", + "destination": "/docs/guides/langchain-middleware", + "permanent": true + }, { "source": "/guides/langgraph-adapter", "destination": "/docs/guides/langgraph-adapter", diff --git a/docs/adapters.md b/docs/adapters.md index 0d4abe8..7f7c99e 100644 --- a/docs/adapters.md +++ b/docs/adapters.md @@ -26,6 +26,7 @@ rather than growing by one each time a framework is named. | Distribution | Framework | Shape | Reuses | Binding | |---|---|---|---|---| +| `ctrlrun-langchain` | LangChain | the call itself is handed over | `AgentMiddleware.wrap_tool_call`, so `handler` **is** the executor | **prevention** | | `ctrlrun-langgraph` | LangGraph | resumed in place | `interrupt()` + `Command(resume=...)`, and the checkpointer | **prevention** | | `ctrlrun-openai-agents` | OpenAI Agents SDK | decided before invocation | the tool-approval interruption | **attribution** | diff --git a/docs/guides/langchain-middleware.mdx b/docs/guides/langchain-middleware.mdx new file mode 100644 index 0000000..d589798 --- /dev/null +++ b/docs/guides/langchain-middleware.mdx @@ -0,0 +1,109 @@ +--- +title: "Use the LangChain middleware" +description: "Gate every tool call a LangChain agent makes with ctrlrun-langchain, through wrap_tool_call, so a refused call never runs and every decision leaves a receipt." +--- + +This guide provides a quick overview for getting started with the CTRLRun [middleware](https://docs.langchain.com/oss/langchain/middleware/overview). CTRLRun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. + +## Overview + +### Details + +| Class | Package | Serializable | Downloads | Version | +| :--- | :--- | :---: | :---: | :---: | +| `CTRLRunMiddleware` | [`ctrlrun-langchain`](https://pypi.org/project/ctrlrun-langchain/) | beta/❌ | ![PyPI - Downloads](https://img.shields.io/pypi/dm/ctrlrun-langchain?style=flat-square&label=%20) | ![PyPI - Version](https://img.shields.io/pypi/v/ctrlrun-langchain?style=flat-square&label=%20) | + +### Features + +- **Policy-gated tool calls** — a refused call never reaches the tool, and the model is told which rule refused it +- **Once stays once** — an effect key executes at most once, across processes sharing a store +- **Unknown outcomes stay unknown** — a tool that raises leaves the effect unresolved rather than retried +- **Human approval** — a policy decision of `approve` holds the call for a person +- **A receipt for every decision** — requests, decisions and results, refusals included + +--- + +## Setup + +No account and no API key. CTRLRun is a library, and the policy is a file in your repository. + +### Installation + +```bash +pip install ctrlrun-langchain +``` + +### Write a policy + +`ctrlrun.yaml` says how much autonomy each tool gets. Unknown tools are denied; there is no default-allow. + +```yaml +schema: ctrlrun.policy/v2 +actions: + lookup_order: + decision: allow + issue_refund: + effect: "refund:{payment_id}" + rules: + - when: { amount_gte: 0, amount_lte: 5000 } # up to €50.00, autonomous + decision: allow + - when: { amount_gte: 0, amount_lte: 500000 } # up to €5,000.00, ask a human + decision: approve + - decision: deny +``` + +## Instantiation + +```python +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_order, issue_refund], + middleware=[CTRLRunMiddleware(control)], +) +``` + +## Invocation + +```python +import ctrlrun + +with ctrlrun.context(agent="support-agent"): + result = agent.invoke({"messages": [{"role": "user", "content": "refund order 4471"}]}) +``` + +Every protected call needs a principal: who is acting is an authorization input, so a call without one is denied before the policy is consulted. `ctrlrun.context(...)` supplies it in development; in production an identity provider verifies a credential instead. + +## What the agent sees + +The middleware uses [`wrap_tool_call`](https://docs.langchain.com/oss/langchain/middleware/custom), so a refused call is short-circuited — the tool is never invoked, and the model receives a `ToolMessage` explaining why: + +```text +issue_refund amount=900000 CTRLRun refused this call: rule[2]. 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 CTRLRun refused this call: this effect is already committed +``` + +That last line is the property worth knowing about. Because `handler` is the executor, the effect is reserved before the tool runs and committed from what it returned. Two agents sharing a store cannot both execute the same effect key, and a tool that raises leaves the outcome `AMBIGUOUS` rather than `FAILED` — so the retry is refused until a person resolves it, instead of becoming a double charge. + +## Approvals + +Where the policy says `approve`, the call is held and the model is told how to release it: + +```text +CTRLRun is holding this call for a human. Approve it with 'ctrlrun approve apr_...', +then ask again. The tool did not run. +``` + +To have the human answered *inside* the run instead, use [`ctrlrun-langgraph`](https://pypi.org/project/ctrlrun-langgraph/), which routes the approval through LangGraph's `interrupt()` and re-presents the same proposal on resume. + +## API reference + +- [Adapters](/docs/adapters), and [the LangGraph adapter](/docs/guides/langgraph-adapter) for approvals answered inside the run +- [`ctrlrun-langchain` source](https://github.com/CTRLRun/ctrlrun/tree/main/adapters/langchain) From 3a5ec2b1ca2f2d40060873e6fa5e0bc024bbc808 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 16 Sep 2026 14:45:28 +0530 Subject: [PATCH 2/2] The page keeps the site's conventions: a search-length description, its SEO row, a Next section Four tests said so: the description was 158 characters against a cap of 155, SEO.md had no row for the page, and every page ends in a Next section that links Why and Get started. The SEO sentence is the page's own opener, as the file requires. Signed-off-by: arpan --- SEO.md | 1 + docs/guides/langchain-middleware.mdx | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/SEO.md b/SEO.md index c5f2690..f449404 100644 --- a/SEO.md +++ b/SEO.md @@ -75,6 +75,7 @@ that page's frontmatter, never here. | `docs/guides/run-on-postgres` | ctrlrun postgres | Use Postgres when workers on more than one host must share one store. | | `docs/guides/verify-in-ci` | verify agent safety configuration CI | `ctrlrun verify` runs the kernel's own failure scenarios against your policy. | | `docs/guides/export-to-opentelemetry` | opentelemetry AI agent actions | `OTelEventSink` turns every action into one OpenTelemetry span. | +| `docs/guides/langchain-middleware` | langchain middleware tool call policy | CTRLRun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. | | `docs/guides/langgraph-adapter` | langgraph interrupt human approval | `ctrlrun-langgraph` makes an `approve` decision surface as a LangGraph `interrupt()`. | | `docs/guides/openai-agents-adapter` | openai agents sdk tool approval | `ctrlrun-openai-agents` makes an `approve` decision stop the run with the SDK's own `ToolApprovalItem`. | | `docs/cookbook/index` | AI agent policy examples | Each recipe is a situation an agent is put in. | diff --git a/docs/guides/langchain-middleware.mdx b/docs/guides/langchain-middleware.mdx index d589798..924b40a 100644 --- a/docs/guides/langchain-middleware.mdx +++ b/docs/guides/langchain-middleware.mdx @@ -1,6 +1,6 @@ --- title: "Use the LangChain middleware" -description: "Gate every tool call a LangChain agent makes with ctrlrun-langchain, through wrap_tool_call, so a refused call never runs and every decision leaves a receipt." +description: "Gate every LangChain tool call with ctrlrun-langchain, through wrap_tool_call, so a refused call never runs and every decision leaves a receipt." --- This guide provides a quick overview for getting started with the CTRLRun [middleware](https://docs.langchain.com/oss/langchain/middleware/overview). CTRLRun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. @@ -103,7 +103,8 @@ then ask again. The tool did not run. To have the human answered *inside* the run instead, use [`ctrlrun-langgraph`](https://pypi.org/project/ctrlrun-langgraph/), which routes the approval through LangGraph's `interrupt()` and re-presents the same proposal on resume. -## API reference +## Next -- [Adapters](/docs/adapters), and [the LangGraph adapter](/docs/guides/langgraph-adapter) for approvals answered inside the run -- [`ctrlrun-langchain` source](https://github.com/CTRLRun/ctrlrun/tree/main/adapters/langchain) +- [Use the LangGraph adapter](/docs/guides/langgraph-adapter): approvals answered inside the run, through `interrupt()`. +- [Adapters](/docs/adapters): the three ways in, and why this one is prevention. +- [The middleware's README](https://github.com/CTRLRun/ctrlrun/blob/main/adapters/langchain/README.md) · [Get started](/docs/get-started/quickstart) · [Why](/docs/why).