Skip to content
datarekha

MAF middleware — guardrails, logging, rate limits

Middleware in MAF wraps around agent invocations and tool calls. Use it for logging, rate limiting, PII scrubbing, and termination. Order matters.

Written and reviewed by Last reviewed

7 min read Advanced Agentic AI Lesson 41 of 78

What you'll learn

  • What middleware is and where it sits in the agent lifecycle
  • The common middleware patterns — logging, rate limit, guardrails, termination
  • Composing middleware and why order matters
  • A working guardrail that scrubs API keys before they reach the model

Before you start

Once you’ve built an agent that works, the next problems are the boring-but-critical ones: logging every call, blocking PII before it leaves your network, rate-limiting per user, killing runaway loops. MAF handles these with middleware — functions that wrap around agent invocations and tool calls, running before the agent receives input and after it produces output, without touching the agent’s own code. Think of middleware as a series of transparent sleeves stacked around the call: each sleeve can inspect, modify, or short- circuit the request as it passes through. If you’ve used ASP.NET, Express, or FastAPI middleware, the concept transfers directly.

Where middleware sits

There are two places middleware runs:

TypeWrapsUse it for
Agent middlewareEach agent.run(...) callAuth, rate limits, input validation, response logging
Function middlewareEach tool invocationTool-specific guardrails, retries, audit logs, cost tracking

Both are functions (or classes) that receive a context object and a next callable. You inspect, optionally mutate, call next() to continue the chain, and inspect/mutate the result on the way back out.

A guardrail that scrubs API keys

The most common production middleware: scan inputs for secrets before they hit the model. If a user pastes an API key into a chat, you do not want it logged, embedded, or echoed back.

# requires: pip install agent-framework (for the commented agent wiring)
# The scrub() function below is plain Python — the middleware wiring is the
# real MAF shape.

import re

# A regex for common secret shapes. Real production uses something like
# Microsoft Presidio or a managed secret scanner — this is the toy version.
SECRET_PATTERNS = [
    re.compile(r"sk-[a-zA-Z0-9]{20,}"),                 # OpenAI-style
    re.compile(r"AKIA[0-9A-Z]{16}"),                    # AWS access key
    re.compile(r"ghp_[a-zA-Z0-9]{36}"),                 # GitHub PAT
    re.compile(r"xox[baprs]-[a-zA-Z0-9-]+"),            # Slack token
]

def scrub(text: str) -> tuple[str, list[str]]:
    """Replace secrets with [REDACTED]. Returns (clean_text, hits)."""
    hits = []
    for pat in SECRET_PATTERNS:
        for m in pat.finditer(text):
            hits.append(m.group()[:6] + "…")  # log first 6 chars for audit
        text = pat.sub("[REDACTED]", text)
    return text, hits

# The middleware itself. Signature: (context, next) -> None.
async def secret_scrubber_middleware(context, next):
    # 1. Inspect/mutate inputs BEFORE the agent runs.
    for msg in context.messages:
        if msg.role == "user" and isinstance(msg.content, str):
            clean, hits = scrub(msg.content)
            if hits:
                print(f"audit: scrubbed {len(hits)} secret(s): {hits}")
                msg.content = clean

    # 2. Call the next thing in the chain (other middleware, then the agent).
    await next()

    # 3. Optionally inspect the response on the way out.
    last = context.result.messages[-1]
    if isinstance(last.content, str):
        last.content, _ = scrub(last.content)  # belt + suspenders

# Wire it in:
#   from agent_framework import ChatAgent
#   agent = ChatAgent(
#       chat_client=chat_client,
#       instructions="Help the user.",
#       middleware=[secret_scrubber_middleware],
#   )

# Sanity check on the scrub function itself (this DOES run):
sample = "My key is sk-abc123def456ghi789jkl0 — help me debug."
clean, hits = scrub(sample)
print(clean)
print(hits)
My key is [REDACTED] — help me debug.
['sk-abc…']

The pasted key is replaced with [REDACTED] before the model (or any log) ever sees it, and the audit list keeps only the first six characters (sk-abc…) — enough to trace which key leaked without storing the secret itself. The structure is the same for every middleware: inspect → call next → inspect the result. Mutate the context to change inputs; mutate the result to change outputs.

Other middleware you’ll write

Logging / telemetry. Log every invocation with user_id, agent_name, input length, output length, latency, token cost. MAF emits OpenTelemetry spans automatically, but custom middleware is where you attach business-specific attributes (tenant, feature flag, A/B variant).

Rate limiting. Check Redis for count(user_id, minute) before calling next(). Raise or return a “rate limit exceeded” response if over the cap. This needs to run early in the chain so rate-limited requests don’t waste downstream work.

Termination / turn cap. For workflows or multi-turn agents, count turns in the context and short-circuit after N. Catches runaway loops that other middleware missed.

Cost guard. Track token spend per user per day. If a user crosses their budget, return a polite “quota exhausted” message instead of calling the model.

Function middleware for auditing tool calls. Every time the delete_user tool runs, write to an immutable audit log: (timestamp, user_id, tool_name, args, approved_by). This is the forensic record you’ll want if someone asks “why was this account deleted?” six months later.

Order matters

Middleware composes in the order you pass it — first one wraps the outside, last one wraps closest to the agent. Mental model: like nested function calls.

agent = ChatAgent(
    chat_client=chat_client,
    middleware=[
        auth_middleware,           # outermost — reject before anything else runs
        rate_limit_middleware,     # cheap, fast — fail-fast for limited users
        secret_scrubber_middleware,
        logging_middleware,        # logs the (already scrubbed) input
        # ... agent runs here ...
    ],
)

The rules of thumb:

  1. Cheap, fail-fast middleware first. Auth and rate-limiting reject requests before you spend cycles on the rest.
  2. Mutating middleware before logging. Otherwise your logs capture the pre-mutation state and your audit trail is wrong.
  3. Cost/quota guards close to the agent. They need accurate estimates of what’s about to be called.
first added runs first on the way in — and last on the way outrequestauthrate limitscrubberloggingAGENTresponse unwinds: logging → scrubber → rate limit → auth

Function middleware — same idea, narrower scope

Function middleware wraps tool calls instead of the whole agent run. Useful when you want a guardrail that only applies to a subset of tools.

async def database_write_audit(context, next):
    """Audit every database mutation tool call."""
    if context.tool_name.startswith("db_write_"):
        audit_log.write(
            user=context.user_id,
            tool=context.tool_name,
            args=context.arguments,
            ts=datetime.utcnow(),
        )
    await next()

You attach function middleware similarly — typically via a function_middleware=[...] parameter on the agent or workflow.

In one breath

  • Middleware wraps agent runs and tool calls — (context, next) functions that inspect/mutate input, call next(), then inspect/mutate the result, without touching agent code.
  • Two scopes: agent middleware (per agent.run() — auth, rate limits, response logging) and function middleware (per tool call — tool guardrails, audit logs).
  • Order matters — middleware nests like function calls: cheap fail-fast (auth, rate limit) first, mutating middleware before logging, cost/quota guards closest to the agent.
  • The canonical guardrail is a secret scrubber on both input and output (defense in depth) — it must run before the logger so raw secrets never hit your logs.
  • Test middleware like tools — an over-eager scrubber or rate limiter silently breaks legit requests.

Quick check

Quick check

0/3
Q1What's the right order: secret-scrubber middleware vs. logging middleware?
Q2Difference between agent middleware and function middleware?
Q3Why put auth middleware first in the chain?

Next

That’s the MAF series. Next up: Google’s Agent Development Kit (ADK), the Gemini-native alternative — same problems, different ergonomics.

Sign in to track your progress

Completed lessons, your XP, level, and streak save to your account — it's free and takes a few seconds.

Practice this in an interview

All questions
How do MCP tool poisoning, cross-server shadowing, and rug pulls differ?

Tool poisoning is malicious instruction content in a tool description or result; cross-server shadowing uses one server’s names or content to influence or misroute another server’s capability; a rug pull changes a previously reviewed capability later. Defenses combine origin-aware namespaces, isolated trust contexts, capability snapshots and change review, exact-argument authorization, sandboxing, and runtime monitoring.

What is tool use or function calling in LLMs, and how do you design good tools for an agent?

Tool use lets an LLM emit a structured request for an external function, which the application validates, authorizes, executes, and returns to the model. Reliable tools have clear descriptions, narrow scope, strict typed inputs, least-privilege access, idempotency, and useful structured errors.

What is an AI agent, and how does it differ from a single LLM call?

An AI agent is an application that lets an LLM choose and execute validated tools in a bounded loop, carrying observations and state forward until it reaches a goal or needs approval. A single LLM call produces one response or tool-call proposal and stops; it does not itself provide the loop, live-system access, memory, or side effects.

Why is OAuth token passthrough dangerous in an MCP server?

In a normal MCP deployment, blindly forwarding the client’s bearer token to a downstream API violates the token’s audience boundary and can create confused-deputy, replay, and audit risks. The server should validate a token intended for itself, authorize the tool call, and obtain a separate, narrowly scoped credential for the downstream resource.

Related lessons

Explore further