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.
Mistral returns tool_calls. Your dispatcher runs them. There is no checkpoint between "the model chose this" and "this side effect happens".
Tickets, emails, and PR descriptions feed straight into the agent. Adversarial content can override system instructions and steer destructive function calls.
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.
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.
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.
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
Install the SDK
pip install veto-sdk mistralaiDefine policies
Create veto/policies.yaml with rules for each Mistral function. Match on name, constrain arguments, set actions.
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?
What about Mistral's built-in JSON schema validation?
How does this interact with tool_choice='required'?
Can I use different policies per environment?
Related integrations
Wrap one Mistral tool path and inspect the decision record.