Mi

Mistral runtime authorization

Wrap Mistral function calls with Veto. Each governed tool the model picks is evaluated before dispatch: allow, review, or deny, with an exportable decision record per governed decision.

The problem with Mistral function calling

Mistral supports production function-calling workflows: it follows JSON schemas tightly and chains tools across multi-turn conversations. That same precision is the attack surface. The model decides which function to call and what arguments to pass. The Mistral SDK hands you back tool_calls and your code runs them. There is no guardrail step in between.

For an SRE agent or a data-platform copilot wired to execute_sql,rotate_secret, or kubectl_apply, that gap is the difference between a useful assistant and a destructive one. A single prompt-injected ticket: pasted into the agent's context for triage: can cause Mistral to emit execute_sql({statement: "DROP TABLE customers"}) with perfectly valid JSON.

No pre-execution auth

Mistral returns tool_calls. Your dispatcher runs them. There is no checkpoint between "the model chose this" and "this side effect happens".

Prompt injection in context

Tickets, emails, and PR descriptions feed straight into the agent. Adversarial content can override system instructions and steer destructive function calls.

No decision record

Mistral logs requests on its side, but you have no record of what was attempted, what was allowed, and what was refused. SOC 2 and EU AI Act both require that.

Before and after Veto

The left tab shows a standard Mistral agent dispatching function calls. The right tab routes each governed call through veto.guard() before executing. Same tools, same model, same conversation loop: governed side effects now evaluated against policy.

py
import os
from mistralai import Mistral
import json

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

tools = [
    {
        "type": "function",
        "function": {
            "name": "execute_sql",
            "description": "Run a SQL statement against the production database.",
            "parameters": {
                "type": "object",
                "properties": {
                    "statement": {"type": "string"},
                    "database": {"type": "string"},
                },
                "required": ["statement", "database"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "rotate_secret",
            "description": "Rotate a service account secret in Vault.",
            "parameters": {
                "type": "object",
                "properties": {
                    "secret_path": {"type": "string"},
                    "service": {"type": "string"},
                },
                "required": ["secret_path", "service"],
            },
        },
    },
]

def execute_sql(statement: str, database: str) -> str:
    return db.execute(statement, database=database)

def rotate_secret(secret_path: str, service: str) -> str:
    return vault.rotate(secret_path, service=service)

response = client.chat.complete(
    model=os.environ["MISTRAL_MODEL"],
    messages=[{"role": "user", "content": user_message}],
    tools=tools,
    tool_choice="auto",
)

# Mistral picks a function. You execute the JSON it returns. No safeguards.
# A prompt injection can run "DROP TABLE customers" through execute_sql.
for call in response.choices[0].message.tool_calls or []:
    args = json.loads(call.function.arguments)
    if call.function.name == "execute_sql":
        result = execute_sql(**args)
    elif call.function.name == "rotate_secret":
        result = rotate_secret(**args)

Multi-turn agent loop

Mistral function calling is most useful inside a multi-turn loop where the model iterates between thinking, calling tools, and reading results. Veto sits inside the dispatch step, so the loop topology stays intact: denials and approvals are returned as tool messages and the model can react.

mistral_agent_loop.py
import os
from mistralai import Mistral
from veto_sdk import Veto
import json

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

def run_agent(user_message: str, max_turns: int = 8):
    messages = [
        {"role": "system", "content": "You are an SRE assistant. Use tools to investigate."},
        {"role": "user", "content": user_message},
    ]
    ctx = {"environment": "production", "user_role": "sre"}

    for _ in range(max_turns):
        response = client.chat.complete(
            model=os.environ["MISTRAL_MODEL"],
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg.model_dump())

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            decision = veto.guard(
                tool=call.function.name,
                arguments=args,
                context=ctx,
            )
            if decision.decision == "allow":
                output = execute_tool(call.function.name, args)
            elif decision.decision == "require_approval":
                output = f"Pending approval {decision.approval_id}: {decision.reason}"
            else:
                output = f"Blocked: {decision.reason}"

            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "name": call.function.name,
                "content": output,
            })

    return "Max turns reached"

Policy configuration

Define what your Mistral agents can and cannot do. Declarative YAML, version controlled, no prompt engineering. The example below is tuned for a production database + secrets agent.

veto/policies.yaml
rules:
  - name: block_production_database_deletes
    description: Refuse DROP, DELETE, TRUNCATE on production
    tool: execute_sql
    when: |
      context.environment == "production" &&
      (args.statement.upper().contains("DROP TABLE") ||
       args.statement.upper().contains("TRUNCATE") ||
       args.statement.upper().startswith("DELETE"))
    action: deny
    message: "Destructive SQL is not allowed on production databases"

  - name: scope_database_access
    description: Limit which databases Mistral can touch
    tool: execute_sql
    when: "!(args.database in ['analytics', 'sandbox'])"
    action: require_approval
    approvers: [platform-team]
    message: "Database access outside analytics/sandbox needs approval"

  - name: block_schema_changes
    description: Refuse ALTER and CREATE TABLE on production
    tool: execute_sql
    when: |
      context.environment == "production" &&
      (args.statement.upper().contains("ALTER TABLE") ||
       args.statement.upper().contains("CREATE TABLE"))
    action: deny
    message: "Schema changes must go through migrations, not the agent"

  - name: secret_rotation_high_value
    description: High-value services need security approval to rotate
    tool: rotate_secret
    when: args.service in ["stripe", "auth-service", "vault-root"]
    action: require_approval
    approvers: [security-team]

  - name: block_root_secret_paths
    description: Never let the agent touch root secrets
    tool: rotate_secret
    when: args.secret_path.startswith("/secrets/root") || args.secret_path.contains("master-key")
    action: deny
    message: "Root and master secrets are out of scope for agents"

  - name: read_only_for_viewer
    description: Viewers can read but not run mutating SQL
    tool: execute_sql
    when: |
      context.user_role == "viewer" &&
      !args.statement.upper().startswith("SELECT")
    action: deny
    message: "Viewer role is read-only"

First governed call

1

Install the SDK

pip install veto-sdk mistralai
2

Define policies

Create veto/policies.yaml with rules for each Mistral function. Match on name, constrain arguments, set actions.

3

Call veto.guard() in the dispatcher

Before executing the function returned in tool_calls, call veto.guard(). Execute only if the decision is allow; return a tool message otherwise so Mistral can adjust.

What Veto covers for Mistral agents

Every Mistral model

Mistral hosted and open-weight variants you self-host. Veto runs inside your dispatcher, so the model behind the function call is irrelevant.

Argument constraints

Pattern-match SQL statements, scope kubectl namespaces, restrict secret paths, bound transaction amounts. Policy decides on data, not on prompt phrasing.

Human review

Route sensitive function calls: production schema changes, root secret rotations, customer-data exports: to an approval queue. The agent loop sees a pending tool message and can either wait or pick a different path.

Decision record

Each guarded Mistral function call can be recorded with name, arguments, model, decision, reason, and timestamp. Decision records are queryable via API and exportable for SOC 2, HIPAA, and EU AI Act reviews.

Frequently asked questions

Does Veto work with self-hosted Mistral weights?
Yes. Veto runs in your Python process and decides on tool calls regardless of where the model is hosted. If you serve open Mistral-compatible weights via vLLM or TGI behind a chat-completions adapter, the same veto.guard() call applies to the resulting tool_calls payload.
What about Mistral's built-in JSON schema validation?
Mistral validates that the model's output matches the function's parameter schema. That is a syntactic check. Veto enforces semantic policy on top: schema-valid arguments can still be destructive, exfiltrative, or outside the user's role. Use both.
How does this interact with tool_choice='required'?
tool_choice='required' forces Mistral to emit a tool_call, but it does not constrain which one or with what arguments. Veto still evaluates the resulting call. If denied, you return a tool message and the next loop turn can pick a different function or give up.
Can I use different policies per environment?
Yes. Pass context={'environment': 'production'} (or staging/dev) into veto.guard(). Rules condition on context.environment. Same dispatcher code, different policy outcomes.

Related integrations

Wrap one Mistral tool path and inspect the decision record.