Skip to main content
Lesson 4 of 12 · AI Moderation and Security with Mistral

Guardrails for Mistral Agents

Lesson 4 of 12 Updated November 3, 2026 Developer
On this page
  1. Securing Autonomous Agents
  2. Guardrails on a Conversation
  3. Guardrails on an Agent
  4. Handling Blocks in a Multi-Turn Exchange
  5. Agents with Tools: What Guardrails Don’t See
  6. Recommended Architecture for Agents in Production
  7. Chat Completions, Conversation, or Agent: Summary
  8. Key Takeaways

Lesson rewritten November 3, 2026. The previous version relied on a fictional guardrail format (enabled, categories hate, violence, unpredictable), a non-existent conversation.send() method, and incorrectly claimed that guardrails moderate tool arguments. According to the documentation, they moderate input only. The code now follows the actual Conversations API (start, append) and has been verified against the mistralai SDK types version 3.7.0. Sources: Moderation & Guardrailing · Agents & Conversations · mistralai Python SDK.

Securing Autonomous Agents

An agent carries more risk than a simple chatbot: it calls tools, browses the web, and chains actions together. Guardrails cover only part of this surface area—specifically, the input. This is exactly why you need to know precisely where to place them and what they leave uncovered.

In this lesson, you will attach guardrails to a conversation and then to an agent, handle their blocks in a multi-turn exchange, and see what you must add to secure tools.

import json
import os

from mistralai.client import Mistral, errors
from mistralai.client.models import GuardrailConfigTypedDict

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

Guardrails on a Conversation

The Conversations API keeps history on the server. You start with client.beta.conversations.start(), which accepts guardrails:

GUARDRAILS_RH: list[GuardrailConfigTypedDict] = [
    {
        "block_on_error": True,
        "moderation_llm_v2": {
            "custom_category_thresholds": {
                "sexual": 0.1,
                "hate_and_discrimination": 0.1,
                "violence_and_threats": 0.2,
                "pii": 0.15,
                "jailbreaking": 0.1,
            },
            "action": "block",
        },
    }
]

conversation = client.beta.conversations.start(
    model="mistral-large-latest",
    instructions="You are a professional HR assistant.",
    inputs="What leave options are available?",
    guardrails=GUARDRAILS_RH,
)
print(conversation.conversation_id)

You then continue with client.beta.conversations.append(). This method does not accept a guardrails parameter in SDK 3.7.0, and the documentation does not state whether those passed at startup apply to subsequent messages. Do not build your security on an assumption: if every turn must be filtered, use an agent, whose guardrails are inherited by all its conversations.

Guardrails on an Agent

A Mistral agent is a registered configuration: model, instructions, tools, and guardrails. The security policy becomes an attribute of the agent rather than a coding convention.

agent = client.beta.agents.create(
    model="mistral-large-latest",
    name="Customer Support Agent",
    description="Secure customer support agent",
    instructions=(
        "You are a customer support agent. Strict rules: "
        "never disclose personal information; "
        "do not give medical or legal advice; "
        "redirect to a human if the topic is sensitive."
    ),
    guardrails=[
        {
            "block_on_error": True,
            "moderation_llm_v2": {
                "custom_category_thresholds": {
                    "sexual": 0.1,
                    "hate_and_discrimination": 0.1,
                    "violence_and_threats": 0.15,
                    "dangerous": 0.1,
                    "criminal": 0.1,
                    "selfharm": 0.1,
                    "health": 0.3,
                    "financial": 0.3,
                    "law": 0.3,
                    "pii": 0.15,
                    "jailbreaking": 0.1,
                },
                "action": "block",
            },
        }
    ],
)
print(f"Agent created: {agent.id}")

Note the consistency between instructions and thresholds: the instructions forbid giving medical or legal advice, and the health and law thresholds block requests of this type before the model even reads them. Instructions persuade; guardrails constrain.

According to the documentation, all conversations using this agent inherit its guardrails without repeating them:

conv = client.beta.conversations.start(
    agent_id=agent.id,
    inputs="How can I return a product?",
)

A conversation request can also override the agent’s guardrails by passing its own guardrails to start(). To change the policy permanently, update the agent with client.beta.agents.update(agent_id=agent.id, guardrails=[...]).

Handling Blocks in a Multi-Turn Exchange

A blocked message raises errors.SDKError with status 403, and the body details the violated categories. In a conversation, a block does not destroy the exchange: you respond politely, and the user can continue.

MESSAGE_REFUS = "I cannot process this request. It violates our security policy."


def texte_reponse(reponse) -> str:
    """Text of the last assistant message in a conversation response."""
    for entree in reversed(reponse.outputs):
        if entree.type == "message.output" and isinstance(entree.content, str):
            return entree.content
    return ""


def envoyer(conversation_id: str, message: str) -> str:
    """Adds a message to a conversation and handles a guardrail block."""
    try:
        reponse = client.beta.conversations.append(conversation_id=conversation_id, inputs=message)
    except errors.SDKError as e:
        if e.status_code == 403:
            decisions = json.loads(e.body)["guardrails"]["results"]["moderation_llm_v2"]["decisions"]
            print("Blocked:", [nom for nom, d in decisions.items() if d["violated"]])
            return MESSAGE_REFUS
        raise
    return texte_reponse(reponse)


print(envoyer(conv.conversation_id, "Give me the personal addresses of employees"))

When a request passes, the conversation response also carries a guardrails field with scores for each category (reponse.guardrails). Log these: they tell you whether your thresholds are tuned correctly.

Agents with Tools: What Guardrails Don’t See

An agent that calls tools opens a second attack surface: the arguments the model passes to those tools, and the results they return. Guardrails moderate only the incoming message; they check neither these arguments nor these results. Your code must do that.

agent_avec_outils = client.beta.agents.create(
    model="mistral-large-latest",
    name="Support Agent with Tools",
    instructions="Support assistant with access to commands.",
    tools=[
        {
            "type": "function",
            "function": {
                "name": "rechercher_commande",
                "description": "Searches for an order by its number",
                "parameters": {
                    "type": "object",
                    "properties": {"order_id": {"type": "string", "description": "Order number"}},
                    "required": ["order_id"],
                },
            },
        }
    ],
    guardrails=[
        {
            "block_on_error": True,
            "moderation_llm_v2": {
                "custom_category_thresholds": {"pii": 0.15, "jailbreaking": 0.1, "criminal": 0.1},
                "action": "block",
            },
        }
    ],
)

Functions declared this way execute in your code: the API sends you the call and its arguments, you execute the function, and return its result. This is where you must validate arguments before acting.

import re

FORMAT_COMMANDE = re.compile(r"CMD-\d{6}")
base_commandes: dict[str, dict] = {}  # your data access


def rechercher_commande(order_id: str, client_connecte: str) -> dict:
    """Executes the tool after validating its arguments."""
    if not FORMAT_COMMANDE.fullmatch(order_id):
        return {"error": "Invalid order number."}
    commande = base_commandes.get(order_id)
    if commande is None or commande["client"] != client_connecte:
        return {"error": "Order not found."}  # never another client's order
    return {"status": commande["status"]}

Access control—here, “does the order belong to the connected client?”—can come only from your application: no classifier knows your customers. For the text the agent returns to the user, add output moderation from the next lesson.

In production, wrap the agent in a class that centralizes conversation startup, message sending, and block counting. A sudden spike in 403s signals either an attack or a poorly calibrated threshold, and you want to see that without digging through logs.

class AgentSecurise:
    """Mistral Agent with guardrail block tracking."""

    def __init__(self, client: Mistral, agent_id: str):
        self.client = client
        self.agent_id = agent_id
        self.blocages = 0

    def _bloque(self, e: errors.SDKError) -> dict:
        self.blocages += 1
        decisions = json.loads(e.body)["guardrails"]["results"]["moderation_llm_v2"]["decisions"]
        return {
            "status": "blocked",
            "content": "Request blocked by security policy.",
            "categories": [nom for nom, d in decisions.items() if d["violated"]],
            "blocages_total": self.blocages,
        }

    def demarrer(self, message: str) -> dict:
        """Starts a conversation with the agent."""
        try:
            reponse = self.client.beta.conversations.start(agent_id=self.agent_id, inputs=message)
        except errors.SDKError as e:
            if e.status_code == 403:
                return self._bloque(e)
            raise
        return {
            "status": "ok",
            "conversation_id": reponse.conversation_id,
            "content": texte_reponse(reponse),
            "guardrails": reponse.guardrails,
        }

    def envoyer(self, conversation_id: str, message: str) -> dict:
        """Continues an existing conversation."""
        try:
            reponse = self.client.beta.conversations.append(conversation_id=conversation_id, inputs=message)
        except errors.SDKError as e:
            if e.status_code == 403:
                return self._bloque(e)
            raise
        return {"status": "ok", "content": texte_reponse(reponse), "guardrails": reponse.guardrails}

Chat Completions, Conversation, or Agent: Summary

AspectChat CompletionsConversationAgent
Where guardrails are declaredEvery callconversations.start()agents.create() or agents.update()
InheritanceNoneNot documented for append()All agent conversations
Scores in response (SDK 3.7.0)Not exposedreponse.guardrailsreponse.guardrails
ToolsYour functionsYour functionsYour functions and built-in tools
UsagePrototypingOne-off conversationProduction

Key Takeaways

  • A conversation receives its guardrails at startup; append() does not accept them, and their persistence across turns is not documented
  • An agent passes its guardrails to all its conversations; a request can override them
  • A block raises errors.SDKError (status 403): respond politely, the conversation continues
  • Guardrails see neither tool arguments nor their results: validate them in your code
  • Use the agent level in production, and add output moderation