Documentation

Red-team reports

Tiếng Việt: red-team.vi.md.

Guard Spec improves from texts it gets wrong. This page describes how to report one, what maintainers do with a report, how a reported text becomes training or hold-out data, and how the fix reaches a release.

There are two kinds of report:

Kind Meaning Issue form
Bypass an attack text that was allowed (or only logged) Bypass (prompt got through)
False positive a legitimate text that was blocked, routed or flagged False positive (benign blocked)

Before you report

How to report

From the admin UI. On the Request log page, use Report on a row, or in the row's details. The dialog picks the kind from the decision (an allowed or logged row is a bypass, a blocked or routed one a false positive; you can change it), asks where the text appeared (user prompt, RAG document, tool result, MCP tool description) and shows the fields it will send:

The request log keeps no per-layer scores; POST /guardrail/scan returns scores.l1, l2 and l3 if you want to add them. The request ID, hash, route target and API key are never included.

The text of the request is included only when the organization stores content (Audit privacy) and you tick Include the text after the warning. The dialog then shows the last user message (or the last tool result, for a RAG document or tool result) in an editable box: anonymise it there. Open GitHub issue opens the form in a new tab with these fields filled in; you review it, fill in the language, attack type and expected outcome, and post it yourself. The control plane sends nothing anywhere. If the prefilled link would be longer than GitHub accepts (about 8,000 characters), the report is copied to the clipboard and the empty form opens instead. Every role that can read the request log, viewers included, can report.

Without the admin UI. Open the form directly and fill in the same fields, from the audit row or a POST /guardrail/scan response.

Minimal pairs. A bypass and a benign text that differ in a few words are the most useful reports. Put the other issue's number in Related report so that both land in the same data split.

What happens next

Triage labels

The forms add detection, bypass or false-positive, and needs-triage. A maintainer then removes needs-triage and adds one outcome:

Label Meaning
triage:train accepted; the text goes to the training data (train or validation split)
triage:holdout accepted; the text goes to the community hold-out, never trained on
triage:duplicate the same text or the same technique is already reported; the issue is closed with a link to the first one
triage:rejected not a detector error (expected behaviour, a configuration problem, an upstream model issue) or no usable text
triage:security severe: moved to a private security advisory; the public issue is closed without details

Optional labels during triage:

Training or hold-out

A maintainer decides per report, before anyone trains on it:

Import and deduplication

train/import_issues.py turns triaged issues into staging files in the schema of train/data (train/DATA_CARD.md). It needs no training environment:

# From GitHub (token optional, read from GITHUB_TOKEN or GH_TOKEN, never stored)
uv run --no-project python train/import_issues.py --repo guard-spec/guard-spec

# From a local export, e.g. gh issue list --label triage:train --state all \
#   --json number,title,body,labels,author,url > issues.json
uv run --no-project python train/import_issues.py --from-json issues.json

# Tests (fixtures only, no network)
uv run --no-project python -m unittest discover -s train/tests -v

It writes community_train.jsonl, community_val.jsonl, community_holdout.jsonl and import_report.json to train/runs/community/ (gitignored, --out to change). Each row is

{"id": "en-bfb33ffec3af", "text": "…", "labels": ["prompt_extraction"], "lang": "en", "variant": "orig",
 "source": "community:issue#101", "license": "Apache-2.0", "group": "community.101"}

An issue is refused, with the reason in import_report.json, when it:

The pair-aware split

Reports linked through Related report (directly or in a chain) form one group, community.<lowest issue number>. A whole group goes to the same split, so the benign and the attack text of a minimal pair never end up on both sides of training and measurement. A group with any triage:holdout report goes to the hold-out; the others are split by the same hash as the rest of the training data (sha256("split:" + group), 12% to validation).

eval/ is never touched

Reported texts are never added to eval/, and nothing is tuned on it (CONTRIBUTING.md). The importer reads eval/ only to drop texts that are already there or nearly so. A change to eval/ needs a new eval release with its own provenance, never a community report.

Into the training data

A maintainer reviews the staging files and the report, then commits the accepted rows in a pull request under train/data/community/ (community_train.jsonl, community_val.jsonl, community_holdout.jsonl), which makes them part of the dedupe set of later imports. train/DATA_CARD.md lists their counts and the reporters. The first such pull request also teaches train/data/build_data.py to read that directory (training and validation rows into the pool with their groups, the hold-out as its own split), with the same leakage guard as every other row; until then no community row reaches a model.

How fixes ship

  1. A rule or a model. A narrow, high-precision pattern becomes an L1 rule; anything broader waits for a classifier trained with the new rows. Rules are developed on the reports and on train/data validation splits, never on eval/, and the pull request says which data it used.
  2. Measured on the hold-out. A candidate is scored once on the community hold-out and the other hold-out splits, then on eval/ at the end, with the numbers in the pull request.
  3. Shadow mode. A new classifier runs next to the active one (SHADOW_L2_MODEL, README Shadow mode and model reload): operators see on the Models page where it would block or allow differently on their own traffic, without changing a decision.
  4. Default. After a release with the candidate in shadow mode and no regressions reported, the model manifest recommends it and a later release makes it the default. The CHANGELOG names the reports it fixes.

A reporter can check a fix with the release that names their issue, or earlier with POST /guardrail/scan on a gateway running the candidate.

Credit

Say in the issue if you prefer not to be named.

Source: docs/red-team.md @ f26302340bf1