Back to BlogTutorial

Getting Started with DeerFlow Guardrails: Pre-Tool-Call Authorization in 5 Minutes

How to add deterministic, policy-driven authorization to every tool call in ByteDance's DeerFlow multi-agent system. One config section. Zero code changes. Works with MCP tools.

5 min read
by Uchi Uchibeke

TL;DR

  • DeerFlow now has a GuardrailMiddleware that evaluates every tool call before execution — including MCP tools.
  • One config section in config.yaml enables it. No code changes to your agent.
  • APort's OAPGuardrailProvider is the reference implementation: passport-based, policy-driven, local or hosted.
  • Or bring your own provider — any class with evaluate(request) -> decision works.
  • 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:

  1. Builds a GuardrailRequest with the tool name, arguments, and passport reference
  2. Calls provider.evaluate(request) on whatever provider you've configured
  3. If the provider says deny: returns a ToolMessage with an error — the agent sees the denial and adapts
  4. If the provider says allow: passes through to the real tool handler
  5. 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: