One gate, four frameworks: governing agents below the framework

Over the last week I put four different agent frameworks under the same governance gate and published the receipts one at a time. Read separately they look like four integrations. They’re not, they’re one gate, and the reason it governs all four is the reason it would govern a fortieth with no new gate code at all.

Update, July 16, 2026: warden has since been renamed redstamp (github.com/askalf/redstamp): same firewall, same code, same guarantees; this post keeps the name it shipped with, and every repository link redirects.

Over the last week I put four different agent frameworks under the same governance gate and showed the receipts one at a time: a CrewAI Flow, a LangGraph StateGraph, the OpenAI Agents SDK, and Microsoft AutoGen.

Read separately they look like four integrations. They are not four integrations. They are one gate, and the reason it governs all four is the whole point of this post, because it’s the reason it would govern a fifth, or a fortieth, with no new gate code at all.

The thesis: govern at the boundary below the framework

Every one of these frameworks decides which agent runs, in what order, with what tools. None of them owns the boundary between the agent and its tools, what a call is allowed to do, what’s blocked, and what happened in what order. That boundary is a different job, and it lives one layer down.

All four frameworks speak MCP. askalf’s warden ships warden-mcp: a drop-in stdio proxy that wraps any MCP server. Point a framework’s MCP client at warden-mcp instead of your tool server, and every tool call, and every tool the agent is even allowed to load, is risk-classified, policy-checked, injection-screened, and written to a tamper-evident audit before the tool server ever sees it.

CrewAI Flow ──────┐
LangGraph graph ──┤
OpenAI Agents ────┼──▶ warden-mcp ──▶ MCP tool server
AutoGen agent ────┘   (one gate)      (real tools)
                       classify · policy · screen · audit

The framework doesn’t know the gate is there. It can’t opt out, because the gate isn’t inside it, it’s underneath it, between its MCP client and the tool server. That’s the structural position, and it holds no matter what’s upstream.

The same four guarantees, every time

Each example runs the identical scenario against a notes MCP server that has four real tools and one deliberately poisoned one (exfiltrate_helper, whose description is a prompt-injection instruction to exfiltrate SSH keys). And each one produces the same four receipts:

1. The poisoned tool is stripped before the framework can load it. warden’s tools/list filter runs before the response reaches the client, so the agent’s tool surface comes back as exactly four tools and the fifth is simply gone:

[warden] poisoned tool from server: exfiltrate_helper
  (instruction-override, exfiltration intent, references a sensitive path (.ssh/.env/credentials/...))

2. The benign write is allowed. write_note is classified yellow (a reversible file write), the file really lands on disk, and the read-back returns it intact. This isn’t a gate that blocks everything.

3. The destructive call is blocked at the gate. A run_command asking to recursively wipe the filesystem root is classified black and refused; the tool server never sees it:

⛔ warden blocked this call (black): ☠ recursive force-delete of root/home/system/glob; ⚠ file deletion

4. Every verdict is in a hash-chained audit, and tampering breaks it. Each entry is chained by SHA-256 over the previous one, so flipping a recorded block to allow after the fact breaks the chain at exactly that entry:

1) intact chain  -> {"ok":true,"entries":3}
2) after flipping the blocked verdict to "allow" -> {"ok":false,"at":1}

Same gate, same policy shape, same four outcomes, across four runtimes that share almost nothing.

The proof grid

Every linked cell resolves to a real, public artifact, the PRs are on the public askalf/warden repo, and the write-ups are live.

Framework Native runtime Speaks MCP via Merged PR Write-up
CrewAI Python Flow engine (@start/@listen) mcp.ClientSession PR #15 CrewAI
LangGraph JavaScript state-graph runtime @langchain/mcp-adapters PR #16 LangGraph
OpenAI Agents SDK OpenAI’s own agent loop (Runner) MCPServerStdio PR #17 OpenAI Agents
Microsoft AutoGen multi-agent actor runtime McpWorkbench PR #18 AutoGen

A Python Flow engine, a JS state-graph, OpenAI’s agent loop, and Microsoft’s actor runtime. Four first-party MCP clients, none of them ours. One gate under all of them.

The structural property

Here’s the claim that separates this from a portfolio of integrations, and it’s checkable in about ten seconds: open the “Files changed” tab on any of those four PRs. Every one of them touches only examples/. Not one line of warden’s own source changed to govern a new framework.

Governing CrewAI, then LangGraph, then OpenAI’s SDK, then AutoGen was not four integration projects. It was four example harnesses pointed at the same unchanged gate. The gate went from version 0.2.0 to 0.2.1 across the whole series, a point release, not four adapters. Adding the fourth unrelated framework was structurally the same work as adding the first, because the governance doesn’t live in the framework. It lives one layer down, where every framework looks the same: an MCP client talking to an MCP server, with a gate in between.

That’s why “one primitive, N frameworks” is a property and not a promise. The cost of the next framework is an example, not an integration.

What this is, and, precisely, what it isn’t

I want to be exact about scope, because it’s easy to overclaim.

These are deterministic tool-surface proofs, and the model is not live. In the CrewAI and LangGraph examples the framework’s nodes call the tools directly. In the OpenAI Agents SDK and AutoGen examples a scripted stub model emits the tool calls and the framework’s genuine execution loop (Runner, AssistantAgent) runs them. Either way, no live LLM is choosing the tools and nothing talks to a model API. That’s deliberate: it’s what lets the entire grid run in public CI with no API key, and it’s why the checked-in evidence is reproducible rather than a screenshot. What’s under test is warden’s governance of the calls and each framework’s real execution path, not inference.

That distinction doesn’t weaken the result, because warden classifies the call, not the reasoning that produced it. A recursive-root-delete is black whether a scripted step or a live model emitted it. Swap the stub for a real model, set the API key, drop the custom provider, and the agent, its MCP client, and the gate are byte-for-byte unchanged; only the token source differs. That’s wiring, not research, and the stub stays in the checked-in examples on purpose so anyone can reproduce the evidence without a key or a bill.

And one clear boundary: this is the warden gate specifically, the tool-action boundary. It’s not a claim about a whole deployed security suite. Each post carries full provenance (exact framework and SDK versions) in its evidence/ directory, and every example is runnable.

Why it matters

If you run agents, your framework or someone else’s, the boundary between an agent and its tools is where “allowed,” “blocked,” and “prove it” actually have to live. Putting that boundary inside each framework means re-solving it for every framework, forever. Putting it below the framework, at the protocol, means solving it once. Four unrelated frameworks, one gate, four merged PRs that touch only their own examples, that’s the difference between an integration you maintain and a primitive you own.

The gate is open source at github.com/askalf/warden. The four examples are in examples/. The evidence is checked in, and the audit chains are verifiable by anyone.

We build the control-plane layer for autonomous agents: the boundary between an agent and its tools, where “allowed,” “blocked,” and “prove it” actually live. If you’re running agents (any framework, or ours) and need that boundary to be real, that’s the kind of problem we go deep on.

Start a conversation →
← All writing