{"slug": "when-blocking-an-agent-isn-t-enough-wiring-langchain-into-the-agent-control", "title": "When Blocking an Agent Isn't Enough: Wiring LangChain Into the Agent Control Plane", "summary": "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.", "body_md": "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.\n\nIn 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.\n\nDeclare 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.\n\nOf 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.\n\nThat 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.\"\n\nThat'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.\n\nThe 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.\n\n``` python\nfrom langchain.agents import create_agent\nfrom langchain.agents.middleware import wrap_tool_call\nfrom langchain_core.messages import ToolMessage\nfrom langchain_core.tools import tool\nfrom agent_action_manifest import load_manifest\nfrom agent_control_plane import RunRecorder\n\nmanifest = load_manifest(\"data_pipeline_agent.manifest.json\")\nactions_by_name = {a.action_name: a for a in manifest.actions}\n\n# The manifest already says which declared actions can run unattended\n# (review_requirement.mode == \"none\") and which need a human first.\n# That split becomes the Control Plane's allow/block list.\nallowed_actions = [\n    a.action_name for a in manifest.actions if a.review_requirement.mode.value == \"none\"\n]\nblocked_actions = [\n    a.action_name for a in manifest.actions if a.review_requirement.mode.value != \"none\"\n]\n\nrecorder = RunRecorder()\nrecorder.start_run(\n    task=\"Apply routine schema maintenance to the analytics database.\",\n    actor=\"data-pipeline-agent\",\n    environment=\"production\",\n    allowed_tools=allowed_actions,\n    blocked_tools=blocked_actions,\n    policy_version=manifest.manifest_id,\n)\nrecorder.add_authority_record(actor=\"data-pipeline-agent\", scope=[\"write\"], source=\"data-platform-team\")\n\n@wrap_tool_call\ndef manifest_guard(request, handler):\n    \"\"\"Check every tool call the agent makes against the manifest before it runs.\"\"\"\n    tool_name = request.tool_call[\"name\"]\n    action = actions_by_name.get(tool_name)\n\n    proposal = recorder.propose_action(\n        tool_name=tool_name,\n        action_type=action.action_type if action else \"unknown\",\n        target=\"db_tool\",\n        payload=request.tool_call[\"args\"],\n        reason=f\"Agent requested {tool_name}.\",\n    )\n    decision, _blocked = recorder.evaluate_action(proposal)\n\n    if decision.result != \"allow\":\n        return ToolMessage(\n            content=f\"{decision.result}: {decision.reason}\",\n            tool_call_id=request.tool_call[\"id\"],\n        )\n\n    result = handler(request)\n    recorder.record_reliance(\n        source_name=tool_name,\n        source_type=\"tool\",\n        scope=f\"Executed {tool_name}\",\n        referenced_action_id=proposal.action_id,\n    )\n    return result\n\n@tool\ndef schema_add_column(table: str, column: str, column_type: str) -> str:\n    \"\"\"Add a nullable column to a table.\"\"\"\n    return f\"added column {column} ({column_type}) to {table}\"\n\n@tool\ndef table_drop(table: str) -> str:\n    \"\"\"Drop a table from the database.\"\"\"\n    return f\"dropped {table}\"\n\n@tool\ndef schema_rename_table(table: str, new_name: str) -> str:\n    \"\"\"Rename a table. Not declared in the manifest.\"\"\"\n    return f\"renamed {table} to {new_name}\"\n\nagent = create_agent(\n    model=chat_model,\n    tools=[schema_add_column, table_drop, schema_rename_table],\n    middleware=[manifest_guard],\n)\n```\n\nEach action now has a unique ID: `request.tool_call[\"name\"]` maps directly to a manifest action, no separate lookup needed.\n\nThe 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.\n\nThree 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.\n\nRunning the agent against a task that touches all three produces:\n\n```\nadded column loyalty_tier (text) to customers\nblock: Tool 'table_drop' is explicitly blocked in this frame.\nescalate: Tool 'schema_rename_table' is not in the allowed-tools list and requires manual review.\nFinished the requested schema maintenance.\n```\n\nAll 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.\n\nAfter 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:\n\n```\nactions   3\ndecisions 3\nblocked   1\nreliance  1\nbundle    f97c5f38-01bf-4ce4-8934-edc5a86d4abe\n```\n\nThe blocked record itself is structured and timestamped, not a caught exception:\n\n```\n{\n  \"blocked_id\": \"96c0ab4c-ca8a-42fc-b7bf-c8ed6d87dc8e\",\n  \"action_id\": \"fa10af91-b3eb-4892-a7bf-a09eabfe29a6\",\n  \"run_id\": \"57604579-477d-43f0-a612-7b1f393ceb8f\",\n  \"reason\": \"Tool 'table_drop' is explicitly blocked in this frame.\",\n  \"policy_name\": \"blocked_tool_policy\",\n  \"blocked_at\": \"2026-09-11T02:58:45.594329+00:00\"\n}\n```\n\nSix 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.\n\nEvery 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.\n\nThe [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.", "url": "https://wpnews.pro/news/when-blocking-an-agent-isn-t-enough-wiring-langchain-into-the-agent-control", "canonical_source": "https://dev.to/cognous/when-blocking-an-agent-isnt-enough-wiring-langchain-into-the-agent-control-plane-c43", "published_at": "2026-09-15 00:30:40+00:00", "updated_at": "2026-09-15 01:01:35.193208+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools", "ai-policy"], "entities": ["Cognous", "LangChain", "Open Control Stack", "Agent Action Manifest", "Agent Control Plane", "Replit"], "alternates": {"html": "https://wpnews.pro/news/when-blocking-an-agent-isn-t-enough-wiring-langchain-into-the-agent-control", "markdown": "https://wpnews.pro/news/when-blocking-an-agent-isn-t-enough-wiring-langchain-into-the-agent-control.md", "text": "https://wpnews.pro/news/when-blocking-an-agent-isn-t-enough-wiring-langchain-into-the-agent-control.txt", "jsonld": "https://wpnews.pro/news/when-blocking-an-agent-isn-t-enough-wiring-langchain-into-the-agent-control.jsonld"}}