Red Teaming
Red Teaming runs adversarial tests against your own agents and LLM routes, on a schedule, inside the same governance that covers every other action on the platform. A campaign sends attempts at a target you choose, an oracle decides which of them actually got somewhere, and what survives is a finding: a severity, a message, a remediation, and enough information to reproduce it.
It answers a question the rest of the platform cannot: not what did my agents do, but what could somebody make them do.
Everything on this page is about MITRITY attacking your environment at your request. It is built so that the attempt itself is bounded, attributable and stoppable — and so that the attack content never becomes something you have to store, review or leak.
What a campaign targets
A campaign names one target and stays on it.
| Target kind | Status | What it exercises |
|---|---|---|
llm_route | Available now | A provider route behind the Mitrity LLM Gateway — prompts, responses, and the guardrails between them |
agent_endpoint | Available now | Your own agent, reached over HTTP one turn at a time — what it discloses, whether it drifts, whether it acts outside its scope. See Testing an agent |
mcp_tool | Planned | A single tool surface behind the sidecar |
Alongside the target, a campaign carries:
- A target environment —
development,stagingorproduction. This is a declaration about what the target is, and the platform treats production differently at every step below. - A corpus selection. You choose which classes of attack a campaign draws from; you never author, read or store the attacks themselves. See The corpus.
- Ceilings — an attempt limit, a time limit, a token budget and a spend budget.
- A schedule, or nothing, for a campaign you run by hand.
Where a campaign runs
A campaign runs in one of your environments, and only the runner deployed in that environment runs it. A campaign can also have no environment, for example one created without one or kept after its environment was deleted, and then no runner runs it. A runner is served the campaigns of its own environment and no other, and a finding it reports can name only an agent in that environment or the agent under test the campaign names.
- A campaign runs only while it has an environment. A campaign without one is served to no runner, so resuming it or choosing Run now answers
409 Conflictuntil you give it an environment. - Deleting an environment pauses its campaigns and ends their runs in flight. The campaigns are kept, with their runs and findings, and no longer belong to an environment. Every queued or running run ends as aborted with
campaign_paused. To keep testing, give the campaign an environment and resume it. - Moving a campaign to another environment ends its runs in flight. Every queued or running run ends as aborted with
operator_stop, and the next run starts in the new environment. The campaign keeps its status. Removing a campaign's environment ends its runs in flight the same way and also pauses the campaign if it was active, as deleting the environment does. Finished runs and their findings stay as they were.
Testing an agent
An agent_endpoint campaign points the runner at your own agent instead of at a gateway route. Where a route campaign tests the guardrails — the gateway's verdict on each attempt is the primary signal — an agent campaign tests the agent: what it discloses about its configuration, whether it can be talked off its mission, whether it acts outside its scope or misuses the tools it holds. Those are things a policy cannot see, because they happen inside the agent's own reasoning.
The campaign names four things:
- The endpoint — an
httpsURL. Its host must be on the runner's own allowlist, which is set where the runner is deployed; a campaign can choose the endpoint, it cannot choose a host you did not allow. - The request template — the JSON body the runner posts for each turn.
{{prompt}}is where the turn's text goes and is required.{{session_id}}keeps a multi-turn entry one conversation on the agent's side.{{context}}is where the runner plants its session reference — the canary — and it matters: without a context slot the canary and synthetic-secret oracles cannot fire against this target, and the campaign form says so.{{attempt_id}}and{{turn}}are available for agents that want them. Every placeholder is filled with a JSON-escaped value, so the template stays valid JSON whatever the attempt contains, and the form shows the first turn exactly as it would be sent. - The response path — where the reply text sits in the answer, such as
reply.textorchoices.0.message.content. Leave it empty to scan the whole body. - The agent under test — the agent the findings are about. It is what the assurance score on the agent's page is computed from.
What a campaign never carries is a credential. The runner presents its own, from its own environment, on a header you name; a campaign that tried to include one is refused.
Attempts are driven one turn at a time. Each answer is scored before the next turn is sent, so a session stops at the first oracle that decides and the finding names the turn it happened on. An agent's answer carries no governance verdict, so the gateway-verdict oracle is off for this kind: what decides an attempt is what the agent said — the canary, the synthetic secret, the response expressions. An agent whose own model calls go through the LLM Gateway and that passes the x-mitrity-redteam-attempt header along has its attempts stamped on the audit trail as well.
The same protections apply as for any other campaign: dry run by default, the typed confirmation before a production target leaves it, and the runner's own second switch for production. Point agent campaigns at staging.
Dry run is the default
A new campaign starts in dry run and stays there until you say otherwise.
In dry run every attempt is selected, governed, costed and scored, and none of it reaches the target. You get the full shape of a run — what it would have sent, what the governance pipeline would have done with it, what it would have cost — without a single request touching a live system.
Leaving dry run is a deliberate, one-way step:
- Open the campaign and choose Turn off dry run.
- Type the campaign's name exactly. The confirm button stays disabled until it matches, and the API checks the same name again on its side — a mismatch is rejected there, not just in the browser.
- A campaign whose target environment is
productionshows a stronger warning first: real attempts will reach a system serving real traffic, and everything they trigger is real — blocked calls, alerts, pages and cost.
There is no toggle and no bulk action for this, and it is not something an update to the campaign can do quietly. Turning dry run off is its own operation with its own audit record.
How attempts are governed and budgeted
An attempt is an action, and it goes through the same decision engine as any other action from the same environment. It is evaluated, it is logged, and if your own policies block it, it is blocked — a campaign is not exempt from the governance it is testing. That is the point: a blocked attempt is a result, not an obstacle.
Every run is bounded four ways:
- Attempt ceiling — the maximum number of attempts, whatever the corpus selection would otherwise produce.
- Time limit — a deadline from the moment the run starts.
- Token budget and spend budget — the cost the run may incur. A budget you leave unset is exactly that: no ceiling. The campaign page says so rather than drawing an empty meter.
- Target error rate — a threshold on how often the target itself fails. A target that starts erroring is a target under strain, and the run stops rather than pushing through it.
Abort conditions
A run ends on its own for any of these reasons, and the reason is recorded on the run:
| Reason | What happened |
|---|---|
attempt_ceiling | The run reached its attempt limit |
deadline_exceeded | The run reached its time limit |
token_budget / usd_budget | The run reached a cost ceiling |
target_error_rate | The target's error rate rose above your threshold |
target_unreachable | The target stopped answering |
campaign_paused | The campaign was paused mid-run, or its environment was deleted |
operator_stop | Somebody stopped it, or the campaign was moved out of its environment |
Pausing a campaign stops the current run at its next attempt. It is the fastest way to end a run in progress, and it needs no confirmation.
Attempts that were never sent
A run that stops early leaves planned attempts unsent, and those are counted as not attempted — their own outcome, their own number, never added to blocked.
This distinction is the whole reason the number exists. An attempt that was blocked tells you your governance held. An attempt that was never sent tells you nothing at all about your governance, and reading one as the other would turn a truncated run into a clean bill of health. Wherever the dashboard shows a percentage, it states the attempted set as the denominator and leaves the unsent attempts out of it.
The oracles and how sure they are
An oracle is what decides whether an attempt got somewhere. Each one carries a confidence level, and the confidence is about the method, not the severity.
| Oracle | Decides by | Confidence |
|---|---|---|
canary | A planted marker appearing where it must never appear | Deterministic |
dlp_redaction | Whether content that should have been redacted was | Deterministic |
gateway_verdict | The governance decision the gateway actually recorded | Deterministic |
response_regex | A pattern in the reply text | Heuristic |
tool_call_observed | A tool call that the mission scope forbids, joined from the audit trail | Heuristic |
judge | A model-based assessment of the exchange | Judged |
- Deterministic means the outcome is a fact: a marker either crossed a boundary or it did not.
- Heuristic means a pattern matched and a pattern can be wrong.
- Judged means the verdict is an argument. Judged findings are real findings and worth reading, but they are the ones to weigh before you act on them at scale.
Confidence appears on every finding and on every finding list, so a queue of critical findings is never uniformly trustworthy by default — you can see which ones were proved and which ones were argued.
The corpus, and what you never have to hold
Attempts are drawn from a corpus MITRITY maintains and versions. You never see its contents, and neither does your control plane. A campaign selects classes of attack; the corpus supplies the material; the runner in your environment sends it.
That is a deliberate boundary. A library of working attacks against AI systems is a liability to store, a liability to review and a liability to leak. Keeping it out of your tenant means there is nothing in your account for an attacker to steal and nothing for an auditor to ask you to justify holding.
What a finding carries instead is a reproduction key:
- the corpus entry identifier — an identifier, not the entry;
- the corpus version it came from;
- the mutation applied, or plain for an unmodified attempt;
- the seed.
Those four values reproduce the attempt exactly on a rerun, which is what reproduction is actually for. None of them tells anyone how to write the attack.
What leaves your environment
The runner executes inside your environment and sees the whole exchange. Very little of it comes back.
Stays in your environment: the full prompt, the full response, the complete transcript of a multi-turn attempt, and everything else the target said or did. It is in your own logs, under your own retention, exactly as any other traffic through that route.
Reaches the control plane:
- A SHA-256 hash of the prompt and a SHA-256 hash of the response. They correlate a finding with your own logs and prove two attempts were identical. They reveal nothing about the content.
- A redacted excerpt of at most 512 characters. Redaction happens in your environment, before anything is sent. If redaction could not be performed, nothing is sent and the finding says the excerpt was omitted rather than quietly shipping raw content.
- The metadata: the outcome, the oracle and its confidence, the turn and pressure stage the attempt reached, the reproduction key, and the counts and costs.
The dashboard shows exactly this and nothing more. When you need the full exchange, it is in your environment where it has been all along.
Findings and their lifecycle
A finding is created the first time an oracle proves an attempt reached its goal. It carries a code, a category, a severity, a message describing what happened, and a remediation describing what to change.
Deduplication. Recurrences do not pile up. A repeat of the same weakness — same code, same target, same corpus entry — updates the existing finding: its occurrence count rises and its last-seen time moves forward. One weakness is one row, however many runs find it.
The three states:
- Open. The finding stands and nothing has been decided.
- Accepted risk. You have looked at it and decided to live with it. The finding stays on the compliance view, because an accepted risk is a documented decision, not a closed gap. It is the honest state for something you have chosen not to fix.
- Resolved. The gap is closed. If the same weakness appears again in a later run, it comes back rather than staying quietly resolved.
Both transitions take an optional note that lands in the audit record.
Proposing a detection pattern
Some findings arrive with a proposed indicator — a detection pattern derived from what the attempt planted, never from the attack text. A finding the canary oracle proved proposes the canary's shape as a DLP pattern: bound on the response direction of the route your agent's model calls travel through, it redacts the planted reference before it leaves the model, which is what makes the next run come back clean. Propose pattern on such a finding opens a governance review at /app/governance/<review>.
Proposing changes nothing. Approving the review lands the pattern as an unbound custom pattern in your DLP library — nothing is enforced until you bind it to a policy yourself, exactly as with a pattern you wrote by hand. The review records what the approval did, including when it could not land the pattern and why.
Closing the loop
Approving a review does not resolve the finding. The finding closes when a later run of the same campaign, against the same target, shows the fix: either the enforcement point refused the attempt that produced it, or the attempt was withstood — the runner planted its session reference, delivered it, and none of it came back. Each closes only what it is evidence of: a refusal refutes whatever the attempt produced; a withstood attempt refutes only the canary and synthetic-secret findings it earlier made, never one a response expression decided. The finding is resolved automatically, with the run named in its note.
An accepted risk is a decision on the record and is left alone by this; so is a finding from a run in dry run, which sent nothing and can close nothing.
The guardrail assurance score
The guardrail assurance card on an agent's page, and on a campaign's, answers one question: of the attack classes the latest runs exercised, how many held? It is computed from the latest scored run of each of the subject's campaigns — a finished run that sent real attempts; a dry run sent nothing and does not score.
Each class exercised is exactly one of:
- Held — no finding, and every run that exercised the class was decisive: it finished with nothing unsent, nothing undecided and no target error.
- Failed — a scored run produced a finding in the class. The card shows the strongest confidence behind it, so a class that failed on a deterministic oracle reads differently from one that failed on a judged one.
- Undecided — anything else. A run that stopped early leaves its classes undecided; so does one in which attempts were delivered and no oracle decided them, which is the ordinary case for an agent endpoint whose answers carry no verdict.
Only a verified verdict holds a class. An llm_route target proves it is a MITRITY enforcement point before the first attempt; an agent endpoint is never asked, so a refusal in front of it is its own word. An agent_endpoint campaign's classes are therefore failed or undecided, never held, and the caveat on the card says how many refusals it could not credit. A held class is always backed by the enforcement point's own verdict.
The number is held over exercised — never over planned — and an undecided class is never counted as held. Every undecided class and every campaign that could not be counted is explained in a caveat on the card, so a score of 100% with three caveats reads as what it is.
Compliance view
The compliance tab groups the same findings the way an assessment asks for them: by framework control, and by category.
MITRITY maps corpus entries to five public frameworks:
| Key | Framework |
|---|---|
owasp_llm | OWASP LLM Top 10 |
atlas | MITRE ATLAS |
nist_ai_rmf | NIST AI RMF |
iso_42001 | ISO/IEC 42001 |
eu_ai_act | EU AI Act |
These mappings are MITRITY's own assertion. They express our view of which control a given corpus entry exercises. They are not a certification, not an audit result, and not an assessment endorsed by the frameworks' authors. Use them to organise evidence and to find the gaps worth arguing about — not as a claim of conformance.
An unmapped finding is not a lesser finding. It simply exercises something the frameworks do not have a control for yet.
Roles
Everyone in the organisation can read campaigns, runs and findings. Creating or changing a campaign, running one, pausing it, turning off dry run, and resolving or accepting a finding are owner or manager actions, enforced by the API rather than hidden in the interface.
A user whose access scope is scoped sees only the campaigns that name at least one agent and only agents granted to them, those campaigns' runs, and the findings about their granted agents. A scoped manager creates and changes only such campaigns: writing one that names no agent answers 403 Forbidden, and one that names an agent outside their grants, or a campaign they cannot see, answers 404 Not Found.
Related Documentation
- Injection Detection — the defence red teaming measures on the prompt path
- Mitrity LLM Gateway — the surface
llm_routecampaigns target, and the guardrails they test - DLP Pattern Authoring — where a proposed detection pattern ends up once its review is approved
- Threat Intelligence — indicators from the shared feed, alongside the ones your own runs surface
- Approval Workflows — the review that gates a proposed pattern