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 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 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 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 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.
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}
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 and the 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 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.