When Blocking an Agent Isn't Enough: Wiring LangChain Into the Agent Control Plane Cognous has introduced the Open Control Stack, a four-layer guardrail framework (Declare, Control, Replay, Evidence) designed to constrain AI agents built on LangChain. The system uses an Agent Action Manifest to define which tools an agent may call and which actions require human approval, and an Agent Control Plane that evaluates every proposed action against policy while keeping a permanent record of allow-or-block decisions. The work follows the July 2025 incident in which a Replit coding agent deleted a production database despite explicit instructions not to touch it. An agent that calls tools has stopped being a text generator, it's an actor. Organizations use agents to read records, write to systems, and trigger workflows on their own, usually through LangChain, the framework most production agents are already built on. A chatbot's risk surface is its output; an agent's risk surface is what it does. In July 2025, a Replit coding agent https://www.theregister.com/2025/07/21/replit saastr vibe coding incident/ deleted a production database after being told explicitly not to touch it. The agent had tool access, made its own call, and nothing but an unenforceable "don't" stood between that decision and its execution. That's the failure mode Cognous's Open Control Stack exists to close. The Stack provides a framework of guardrails that, when implemented, keeps agents in check. It's split across four layers: Declare, Control, Replay, Evidence. Declare comes first because before an agent runs, someone has to decide what it's even allowed to attempt. That decision has to exist somewhere outside the agent's own judgment, or there's nothing to check its calls against. The Agent Action Manifest https://dev.to/cognous/the-manifest-that-keeps-your-ai-agent-honest-3e97 is where Cognous puts it: a file that lists an agent's tools, the authority each action needs, and which actions can run on their own versus which need a human to sign off first. Of course, a manifest sitting in a JSON file doesn't stop anything by itself. Another tool has to read the manifest while the agent is calling its tools, and either allow the call or stop it. The last post https://dev.to/cognous/agentic-guardrails-for-langchain-the-manifest-you-didnt-know-you-needed-3b28 built a guard that wraps each LangChain tool function, looks the call up in the manifest, and raises an exception before the tool runs if the action wasn't declared or needs review first. That guard is already a control, just a naive one. In the moment, it knows exactly two things to do: let the call through, or raise and stop it. The moment the process moves on, that decision is gone. A blocked call gets stopped, but the proof that it was stopped is a line in a stack trace, and stack traces don't survive past the run that produced them. Ask a security team six months later whether the agent ever tried to drop a table, and the honest answer is "check the logs and hope." That's the blind spot the Agent Control Plane https://dev.to/cognous/cognous-control-plane-the-layer-that-tells-your-agent-no-3ln9 closes. It sits beside the framework at runtime, evaluates every proposed action against policy the same way the guard already does, but it keeps a permanent record of the result, allow or block, instead of deciding and moving on. The guard from the last post wrapped one Python function at a time: @guard ... had to be added to every tool a developer wanted checked. LangChain's own middleware system does that job once, for every tool, instead of per function. wrap tool call intercepts every tool call an agent makes in one place, so the manifest check moves into a single middleware instance rather than being repeated across every tool definition. python from langchain.agents import create agent from langchain.agents.middleware import wrap tool call from langchain core.messages import ToolMessage from langchain core.tools import tool from agent action manifest import load manifest from agent control plane import RunRecorder manifest = load manifest "data pipeline agent.manifest.json" actions by name = {a.action name: a for a in manifest.actions} The manifest already says which declared actions can run unattended review requirement.mode == "none" and which need a human first. That split becomes the Control Plane's allow/block list. allowed actions = a.action name for a in manifest.actions if a.review requirement.mode.value == "none" blocked actions = a.action name for a in manifest.actions if a.review requirement.mode.value = "none" recorder = RunRecorder recorder.start run task="Apply routine schema maintenance to the analytics database.", actor="data-pipeline-agent", environment="production", allowed tools=allowed actions, blocked tools=blocked actions, policy version=manifest.manifest id, recorder.add authority record actor="data-pipeline-agent", scope= "write" , source="data-platform-team" @wrap tool call def manifest guard request, handler : """Check every tool call the agent makes against the manifest before it runs.""" tool name = request.tool call "name" action = actions by name.get tool name proposal = recorder.propose action tool name=tool name, action type=action.action type if action else "unknown", target="db tool", payload=request.tool call "args" , reason=f"Agent requested {tool name}.", decision, blocked = recorder.evaluate action proposal if decision.result = "allow": return ToolMessage content=f"{decision.result}: {decision.reason}", tool call id=request.tool call "id" , result = handler request recorder.record reliance source name=tool name, source type="tool", scope=f"Executed {tool name}", referenced action id=proposal.action id, return result @tool def schema add column table: str, column: str, column type: str - str: """Add a nullable column to a table.""" return f"added column {column} {column type} to {table}" @tool def table drop table: str - str: """Drop a table from the database.""" return f"dropped {table}" @tool def schema rename table table: str, new name: str - str: """Rename a table. Not declared in the manifest.""" return f"renamed {table} to {new name}" agent = create agent model=chat model, tools= schema add column, table drop, schema rename table , middleware= manifest guard , Each action now has a unique ID: request.tool call "name" maps directly to a manifest action, no separate lookup needed. The Control Plane checks for a literal scope named write . The manifest uses db.schema.write . manifest guard bridges the two names explicitly. This bridging across manifest to Control Plane is currently a manual part of the ingestion process. Three tools get called through a real agent run: one declared and cleared for automatic execution, one declared but requiring approval, and one never declared at all. Running the agent against a task that touches all three produces: added column loyalty tier text to customers block: Tool 'table drop' is explicitly blocked in this frame. escalate: Tool 'schema rename table' is not in the allowed-tools list and requires manual review. Finished the requested schema maintenance. All three show the Control Plane enforcing the manifest's guardrails. When the agent attempts to add a column, the Control Plane approves the action, since it's declared and deliberately allowed. When the agent attempts to drop the table, the Control Plane stops the action as blocked, since it's declared but requires human approval. When the agent tries schema rename table , the action is stopped too, but as an escalation rather than a block: nobody declared it at all, so the Control Plane's default posture kicks in and sends it for human review instead of assuming it's safe. With a human in the loop, a decision can be made on the table renaming. The manifest can be edited to allow or block the agent's access to schema rename table , ensuring clean future runs. After the run completes, the exported run record shows the difference between the Control-Plane-backed guard and the original exception-raising guard. A run record isn't a log someone has to remember to add or go hunting for later. Every LangChain agent built on this middleware produces a run record automatically, on every run: actions 3 decisions 3 blocked 1 reliance 1 bundle f97c5f38-01bf-4ce4-8934-edc5a86d4abe The blocked record itself is structured and timestamped, not a caught exception: { "blocked id": "96c0ab4c-ca8a-42fc-b7bf-c8ed6d87dc8e", "action id": "fa10af91-b3eb-4892-a7bf-a09eabfe29a6", "run id": "57604579-477d-43f0-a612-7b1f393ceb8f", "reason": "Tool 'table drop' is explicitly blocked in this frame.", "policy name": "blocked tool policy", "blocked at": "2026-09-11T02:58:45.594329+00:00" } Six months from now, the answer to "did the agent ever try to drop a table" isn't a memory or a grep through application logs. It's this record, sitting in an exported run next to the one action that actually executed. Every LangChain agent that calls tools faces the same choice this post started with: enforce nothing and hope, or wire in guardrails before the agent gets access. Skip that choice, and the risk isn't abstract. It's a production database gone the way Replit's did, and a "my agent destroyed my SaaS" story of your own. The Agent Action Manifest https://github.com/cogno-us/cognous-agent-action-manifest and the Agent Control Plane https://github.com/cogno-us/cognous-agent-control-plane are two of the four layers Cognous built so that choice doesn't have to be made from scratch. They're open source, and the Open Control Stack repo https://github.com/cogno-us/cognous-open-control-stack has both, with examples to build an integration like this one from. Cloning the repo gives a LangChain agent solid guardrails and a record of every decision the agent makes.