TL;DR
- DeerFlow now has a
GuardrailMiddlewarethat evaluates every tool call before execution — including MCP tools. - One config section in
config.yamlenables it. No code changes to your agent. - APort's
OAPGuardrailProvideris the reference implementation: passport-based, policy-driven, local or hosted. - Or bring your own provider — any class with
evaluate(request) -> decisionworks. - 5 minutes from zero to secured — including the passport.
The problem with unrestricted tool calls
DeerFlow is powerful. It runs autonomous multi-step tasks with bash, web search, file operations, and dynamically-loaded MCP tools. That power is exactly the problem when something goes wrong.
A single compromised tool call can:
- Exfiltrate data via
curl(even inside a Docker sandbox) - Modify files it shouldn't touch
- Execute commands outside the intended scope
- Call MCP tools with unconstrained arguments
Docker sandboxing provides process isolation, but not semantic authorization. A sandboxed bash can still curl your database credentials to an external server. What's missing is a layer that asks: "Is this agent allowed to run this specific tool with these specific arguments?" — before the tool executes.
That's what the new GuardrailMiddleware does.
How it works
DeerFlow's middleware chain already wraps every tool call (error handling, loop detection, clarification). The GuardrailMiddleware slots in as step 5 of 12:
1. ThreadDataMiddleware
2. UploadsMiddleware
3. SandboxMiddleware
4. DanglingToolCallMiddleware
5. → GuardrailMiddleware ← (new)
6. SummarizationMiddleware
7. TodoListMiddleware
...
Before any tool executes, the middleware:
- Builds a
GuardrailRequestwith the tool name, arguments, and passport reference - Calls
provider.evaluate(request)on whatever provider you've configured - If the provider says deny: returns a
ToolMessagewith an error — the agent sees the denial and adapts - If the provider says allow: passes through to the real tool handler
- If the provider crashes: blocks the call (fail-closed by default)
The agent never knows the middleware is there unless a tool call is denied. And when it is, the agent gets structured feedback it can reason about.
Setup: 5 minutes, 3 steps
Step 1: Create a passport (2 minutes)
pip install aport-agent-guardrails
aport setup --framework deerflow
This runs an interactive wizard that creates an OAP passport — a JSON file declaring your agent's identity, capabilities, and operational limits. For example:
{
"passport_id": "550e8400-...",
"spec_version": "oap/1.0",
"status": "active",
"capabilities": [
{ "id": "system.command.execute" },
{ "id": "data.export" },
{ "id": "mcp.tool.execute" }
],
"limits": {
"system.command.execute": {
"allowed_commands": ["git", "npm", "python", "node"],
"max_execution_time": 30
}
}
}
The passport lives at ~/.aport/deerflow/aport/passport.json.
Step 2: Add guardrails to config.yaml (1 minute)
Add this section to your DeerFlow config.yaml:
guardrails:
enabled: true
passport: ~/.aport/deerflow/aport/passport.json
provider:
use: aport_guardrails.providers.generic:OAPGuardrailProvider
Step 3: Start DeerFlow (0 minutes extra)
make dev
That's it. Every tool call is now evaluated against your passport before execution. No code changes. No new dependencies beyond aport-agent-guardrails.
What happens when a tool call is denied
When the agent tries to run bash with a command not in the passport's allowed_commands, the middleware returns:
Guardrail denied: tool 'bash' was blocked (oap.command_not_allowed).
Reason: 'rm' not in allowed_commands. Choose an alternative approach.
The agent sees this as a tool error and adapts — it might try a different approach, ask for clarification, or explain why it can't complete the task. This is much better than a hard crash or silent failure.
Three ways to evaluate
| Mode | What happens | When to use |
|---|---|---|
| Local | Passport file + bash evaluator. Zero network calls. | Dev, CI, air-gapped environments |
| API | Full OAP engine at ~65ms p50. Signed Ed25519 decisions. | Production with audit requirements |
| Hosted | Passport stored in cloud. Global kill switch in <30s. | Teams and enterprise deployments |
All three modes use the same OAPGuardrailProvider. The mode is determined by the APort config at ~/.aport/deerflow/config.yaml (not the DeerFlow config).
The zero-dependency option
Don't want APort? DeerFlow ships with a built-in AllowlistProvider — zero external dependencies:
guardrails:
enabled: true
provider:
use: deerflow.guardrails.builtin:AllowlistProvider
config:
denied_tools: ["bash", "write_file"]
This blocks the named tools and allows everything else. Simple, but no policy evaluation, no passport, no signed decisions.
Securing MCP tools
MCP tools are loaded dynamically from external servers. Before guardrails, once a tool was loaded, there was no way to constrain what it could do. Now, every MCP tool call goes through the same middleware:
Tool: mcp_github_create_issue
Policy: mcp.tool.execute.v1
Decision: ALLOW (server in allowed_servers, tool in allowed_tools)
The mcp.tool.execute.v1 policy pack enforces server allowlists, tool restrictions, parameter validation, and rate limits.
Bring your own provider
The GuardrailProvider protocol is generic. Any class with evaluate and aevaluate methods works:
class MyGuardrail:
name = "my-company"
def evaluate(self, request):
from deerflow.guardrails.provider import GuardrailDecision, GuardrailReason
# Your authorization logic here
if request.tool_name == "bash" and "rm" in str(request.tool_input):
return GuardrailDecision(
allow=False,
reasons=[GuardrailReason(code="oap.command_not_allowed", message="destructive commands blocked")]
)
return GuardrailDecision(allow=True, reasons=[GuardrailReason(code="oap.allowed")])
async def aevaluate(self, request):
return self.evaluate(request)
guardrails:
enabled: true
provider:
use: my_module:MyGuardrail
This is the fully self-sovereign path. Your passport, your policy, your evaluator. No vendor, no API, no network.
What's next
The GuardrailMiddleware integration with DeerFlow is approved (ByteDance collaborator confirmed) at bytedance/deer-flow#1213. The design uses the Open Agent Passport (OAP) specification for decision formats and reason codes, making guardrail decisions interoperable across frameworks.
The same pattern is already live for LangChain, CrewAI, Cursor, and Claude Code. Adding it to your framework takes one config file and a text file.
Links: