Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
87e4ea4
feat(eval): red-team grading blocks against wrong, empty, hedged and …
napetrov Oct 29, 2026
424afa6
fix(eval): redteam skips the attacks a grading block passes by design
napetrov Oct 29, 2026
583ee2c
fix(redteam): inconclusive status, single-literal stuffing, hedge col…
napetrov Oct 29, 2026
8c2ba2b
fix(redteam): negated checks no longer silence wrong, empty and hedge
napetrov Oct 29, 2026
c0e182c
fix(redteam): clear wrong answer of check values, stuff negated checks
napetrov Oct 29, 2026
267a648
fix(redteam): skip the stuffed probe for a negated mode all match
napetrov Oct 29, 2026
006a777
Merge branch 'main' of https://github.com/intel/dokimi into pr281-work
napetrov Oct 29, 2026
0d731b0
fix(redteam): choose the planted wrong answer and the hedge probe by …
napetrov Oct 29, 2026
ec22242
fix(redteam): grade wrong, empty and hedge against the whole block, t…
napetrov Oct 29, 2026
54c8893
fix(redteam): a set with a question that was not measured is inconclu…
napetrov Oct 29, 2026
07d39a1
fix(redteam): bury aliases between decoys instead of listing them for…
napetrov Oct 29, 2026
4624d63
fix(redteam): grade each question with its own block when an id repeats
napetrov Oct 29, 2026
76a252c
feat(redteam): record the judge model and provider in the redteam.v1 …
napetrov Oct 29, 2026
735c64c
refactor(redteam): read check values and build the judge ask through …
napetrov Oct 29, 2026
6a09774
fix(redteam): name -o in the cut-violations line when no artifact is …
napetrov Oct 29, 2026
27558b5
test(redteam): pin the canary of a whole-answer match and of a string…
napetrov Oct 29, 2026
72d04ab
docs(redteam): list the negated mode all exclusion and say where nega…
napetrov Oct 29, 2026
2359e74
test(redteam): run judge checks through run(ask=) and through the --j…
napetrov Oct 29, 2026
90a4f4c
test(redteam): run an ungradable block through the command and pin th…
napetrov Oct 29, 2026
7627b00
test(redteam): check the artifact of a blocked and an inconclusive co…
napetrov Oct 29, 2026
f667fd8
test(redteam): pin the bare stuffed answer of a whole-answer check wi…
napetrov Oct 29, 2026
d9093cd
docs(redteam): changelog fragment says what an inconclusive run is
napetrov Oct 29, 2026
354041d
Address review: answer the review summaries and bot comments in their…
napetrov Oct 29, 2026
24ea369
docs(redteam): refresh two stale comments on the wrong and stuffed at…
napetrov Oct 29, 2026
180a81d
fix(redteam): a question set nested too deep to parse exits 2, not wi…
napetrov Oct 30, 2026
f76e543
fix(redteam): a block with an unrun judge or regex is unprobed, not a…
napetrov Oct 30, 2026
93fa1e5
Merge remote-tracking branch 'intel/main' into pr281-1029
napetrov Oct 30, 2026
ed0950f
test(redteam): a regex past its timeout leaves the probe unscored, wi…
napetrov Oct 30, 2026
1c60d0d
docs: keep the comment blocks this PR adds to two lines
napetrov Oct 30, 2026
442e80d
fix(redteam): a json contains check's stuffed answer goes after a decoy
napetrov Oct 30, 2026
ecc5377
fix(redteam): type the decoy candidates as a tuple of any length (mypy)
napetrov Oct 30, 2026
b12ecba
test(redteam): pin the decoy filter, the two-decoy fallback and the a…
napetrov Oct 30, 2026
77345de
docs(redteam): name the unmeasured case in the exit-code docstring
napetrov Oct 30, 2026
a76a9f6
Merge intel/main into feat/verifier-redteam
napetrov Oct 30, 2026
808c7fb
docs(reference): regenerate cli-interface.json for dokimi eval redteam
napetrov Oct 30, 2026
c2ab619
fix(redteam): a numeric canary, a stuffed list the grader accepts, a …
napetrov Oct 30, 2026
a95644a
Merge remote-tracking branch 'intel/main' into tidy281-redteam
napetrov Oct 31, 2026
53d142f
feat(redteam): use the canonical CLI flags and the route table
napetrov Oct 31, 2026
198244e
fix(redteam): a negated value the matcher reads as another number is …
napetrov Oct 31, 2026
904a1fa
docs(changelog): eval redteam writes its artifact with --out FILE
napetrov Oct 31, 2026
52d4c5d
Merge remote-tracking branch 'intel/main' into tidy281-redteam
napetrov Oct 31, 2026
03d6683
chore(cli): regenerate the CLI snapshot after merging main
napetrov Oct 31, 2026
c3f2452
docs(redteam): name --out, not -o, in the answer-formats table
napetrov Oct 31, 2026
f39ebfc
fix(runs): list redteam.v1 in the runs kinds table
napetrov Oct 31, 2026
297e47a
fix(redteam): probe the stuffed answer only for a negated check
napetrov Oct 31, 2026
0ad171b
test(redteam): assert the stuffed self-check directly, not through run
napetrov Oct 31, 2026
81fcc17
fix(redteam): drop the stuffed probe, keep stuffed_for as a self-check
napetrov Oct 31, 2026
6cb7781
Merge remote-tracking branch 'intel/main' into pr281-land1031
napetrov Nov 1, 2026
69e3449
fix(redteam): address review on the exact canary, zero-weight checks …
napetrov Nov 1, 2026
7441aaf
test(redteam): fold the _LITERAL import into the redteam import line
napetrov Nov 1, 2026
004d6fe
fix(redteam): zero-weight checks do not count, fuzzy canary is bare, …
napetrov Nov 1, 2026
29a65fe
fix(redteam): unsupported checks do not enable the rejection probes
napetrov Nov 1, 2026
d29b7cd
docs(redteam): the table names up to 20 unmeasured questions; --out k…
napetrov Nov 1, 2026
2262188
Merge remote-tracking branch 'intel/main' into feat/verifier-redteam
napetrov Nov 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added

- **`eval redteam` checks that grading blocks reject wrong answers** — grades a planted wrong, an empty and a hedged answer against each question's `grading` block, and a canary right answer built from its one literal check. A wrong answer the grader accepts, or a canary it rejects, is a violation: the command prints pass rates, exits 1 and, with `--out FILE`, writes a `redteam.v1` artifact (it records the judge model and provider). A run in which a graded question was not measured (judge checks without `--judge`, a block of only negated checks) is `inconclusive` and also exits 1. Judge checks are unscored unless `--judge` is given, so the default run makes no model call.
32 changes: 32 additions & 0 deletions dokimi/docs/answer-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ artifact for an `arms run` file. Each block carries `scorer: reference`, and
the run's pass rates and mean scores are under `run_metadata.summary` (or
`reference_scoring`).

## Red-teaming the grading

`eval redteam QUESTIONS.json` grades synthetic answers against each question's
`grading` block and reports how often the grader accepts them:

| Attack | Answer | The grader must |
|---|---|---|
| `canary` | The right answer, built from the one literal check (`match`, `fuzzy`, `exact`, `choice`, `json`); a question with several checks, a `regex`, a `judge` or a `negate` has none | pass it |
| `wrong` | A planted wrong answer, picked per block so that no positive literal check accepts it, asked with the check's own rules (the probe is skipped only when no candidate is clean) | fail it |
| `empty` | `""` | fail it |
| `hedge` | A fixed non-committal sentence; skipped when a positive literal check accepts it | fail it |

An attack the grader accepts, or a canary it rejects, is a violation, and any
violation makes the run `blocked`: the command prints the pass rates and the
violations and exits 1 (`--out FILE` writes `redteam.v1`). A run in which a graded
question was not measured is `inconclusive` and also exits 1: a question whose
`judge` checks have no verdict without `--judge` (no probe scored), or a block
of only negated checks. A `negate` check passes an answer that lacks its value,
so a block of only negated checks gets none of `wrong`, `empty` and `hedge`: it
is marked `negated_only` in the artifact (the table prints a line "N questions with only negated checks: wrong, empty and hedge not run"),
and it has no probe at all. The table names up to 20 questions that were not
measured and counts them as `unprobed`; `--out FILE` keeps the complete list; only a run that measured every graded
question and found no violation is `pass`. An unwritable `--out` exits 2.
A block with a positive check
is graded as written, `threshold` included, so a negated check's share of the score
counts: a block it lifts over the line is blocked.
A probe the grader cannot
score is unscored and does not block. `judge` checks are unscored unless
`--judge` is given, so the default run makes no model call; with `--judge` the
artifact records `judge_model` and `judge_provider` (both `null` without it). Nothing here
changes a score; it tells you whether a grading block is worth trusting.

## What does not carry over

| Format | Lossy |
Expand Down
73 changes: 73 additions & 0 deletions dokimi/docs/reference/cli-interface.json
Original file line number Diff line number Diff line change
Expand Up @@ -5714,6 +5714,78 @@
"subcommand_required": null,
"subcommands": {}
},
"redteam": {
"allow_abbrev": true,
"params": [
{
"action": "_StoreAction",
"choices": null,
"const": null,
"default": null,
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": false,
"key": "questions",
"kind": "argument",
"nargs": null,
"position": 0,
"required": true,
"type": null
},
{
"action": "_HelpAction",
"choices": null,
"const": null,
"default": "==SUPPRESS==",
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": true,
"key": "--help/-h",
"kind": "option",
"nargs": 0,
"position": null,
"required": false,
"type": null
},
{
"action": "_StoreAction",
"choices": null,
"const": null,
"default": null,
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": false,
"key": "--judge",
"kind": "option",
"nargs": null,
"position": null,
"required": false,
"type": "dokimi.commands.redteam._judge_ref#a59ddb60ae31"
},
{
"action": "_StoreAction",
"choices": null,
"const": null,
"default": null,
"group_key": null,
"group_required": null,
"hidden": false,
"is_flag": false,
"key": "--out",
"kind": "option",
"nargs": null,
"position": null,
"required": false,
"type": null
}
],
"path": "dokimi eval redteam",
"subcommand_required": null,
"subcommands": {}
},
"reference": {
"allow_abbrev": true,
"params": [
Expand Down Expand Up @@ -13046,6 +13118,7 @@
"'plan'",
"'plugin_delta'",
"'questions'",
"'redteam'",
"'repo_budget'",
"'repo_environment'",
"'repo_readiness'",
Expand Down
1 change: 1 addition & 0 deletions dokimi/dokimi/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"result": "result.v1",
"repo_budget": "repo_budget.v1",
"task_inspect": "task_inspect.v1",
"redteam": "redteam.v1",
"cli_routes": "cli_routes.v1",
"need_graph": "need_graph.v1",
"eval_coverage": "eval_coverage.v1",
Expand Down
1 change: 1 addition & 0 deletions dokimi/dokimi/cli_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ class Tombstone:
Leaf(("eval", "pairwise"), PAID),
Leaf(("eval", "reference"), PAID),
Leaf(("eval", "native-import"), FREE),
Leaf(("eval", "redteam"), PAID),
Leaf(("eval", "panel-score"), PAID),
Leaf(("dataset", "export"), FREE),
Leaf(("ingest", "docling"), FREE),
Expand Down
5 changes: 2 additions & 3 deletions dokimi/dokimi/commands/answer_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,9 @@ def cmd_questions_import(args: argparse.Namespace) -> None:
def _ask(args: argparse.Namespace):
if args.no_judge:
return None
from dokimi.eval.judge import Judge
from dokimi.eval.judge import judge_ask

llm = Judge(model=args.judge_model, provider=args.judge_provider).llm
return lambda prompt, draw=0, cache_if=None: llm.invoke(prompt, draw=draw, cache_if=cache_if).content
return judge_ask(args.judge_model, args.judge_provider)


def cmd_eval_reference(args: argparse.Namespace) -> None:
Expand Down
4 changes: 4 additions & 0 deletions dokimi/dokimi/commands/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,10 @@ def register(sub, positive_int) -> None:

answer_formats.register_eval(eval_sub, positive_int) # eval reference, eval native-import

from dokimi.commands import redteam

redteam.register_eval(eval_sub) # eval redteam

# eval panel-score
panel_p = eval_sub.add_parser("panel-score", help="Score answers using a multi-judge panel")
panel_p.add_argument("--answers", required=True, help="Path to answers JSON file")
Expand Down
72 changes: 72 additions & 0 deletions dokimi/dokimi/commands/redteam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""``eval redteam``: check that a question set's grading rejects the answers it should.

Reads a questions set (with ``grading`` blocks), grades synthetic attacks against it and prints
the pass rates. Exits 1 when any attack is accepted, a canary is rejected, a graded question was not
measured, or nothing was graded (``inconclusive``); exits 2 on an unreadable input, a malformed
``--judge`` or an unwritable ``--out``. Judge checks are not scored unless ``--judge PROVIDER/MODEL``
is given, so the default run makes no model call.
"""

from __future__ import annotations

import argparse
import sys


def _ask(args: argparse.Namespace):
if not args.judge:
return None
from dokimi.eval.judge import judge_ask

provider, model = args.judge
return judge_ask(model, provider)


def _judge_ref(value: str) -> tuple[str, str]:
from dokimi.eval.judge import JUDGE_PROVIDERS

provider, _, model = value.partition("/")
if provider not in JUDGE_PROVIDERS or not model.strip():
raise argparse.ArgumentTypeError(
f"expected PROVIDER/MODEL with PROVIDER one of {', '.join(JUDGE_PROVIDERS)}, got {value!r}")
return provider, model.strip()


def cmd_eval_redteam(args: argparse.Namespace) -> None:
from dokimi.artifacts import save_artifact
from dokimi.eval.redteam import format_table, run
from dokimi.questions.artifact import load_questions

try:
questions = load_questions(args.questions).artifact.get("questions") or []
result = run(questions, ask=_ask(args))
except (OSError, ValueError, RecursionError) as e: # RecursionError: nested too deep
print(f"Error: {e}", file=sys.stderr)
sys.exit(2)
result["questions_path"] = str(args.questions)
result["judge_model"] = args.judge[1] if args.judge else None
result["judge_provider"] = args.judge[0] if args.judge else None
print(format_table(result, output=args.out))
if args.out:
try:
save_artifact("redteam", result, args.out)
except (OSError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(2)
print(f"Wrote redteam artifact to {args.out}")
if result["status"] != "pass":
sys.exit(1)


def register_eval(eval_sub) -> None:
r = eval_sub.add_parser(
"redteam",
help="Check grading blocks reject wrong, empty and hedged answers and accept a canary")
r.add_argument("questions", help="questions JSON with grading blocks (from `questions import`)")
r.add_argument("--judge", metavar="PROVIDER/MODEL", type=_judge_ref, default=None,
help="Grade judge checks with this judge model (model calls); default leaves them unscored")
r.add_argument("--out", metavar="FILE", default=None, help="redteam.v1 JSON to write")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: the changelog advertises -o, but this parser registers only --out, and shared CLI parsing does not add an alias. dokimi eval redteam questions.json -o redteam.json therefore exits 2 with an unrecognized-argument error instead of writing the artifact. Register -o as an alias (and test that invocation), or consistently document only --out. The same mismatch appears in the added answer-formats documentation.

Suggestion:

Suggested change
r.add_argument("--out", metavar="FILE", default=None, help="redteam.v1 JSON to write")
r.add_argument("--out", "-o", metavar="FILE", default=None, help="redteam.v1 JSON to write")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adding -o. The parser registers --out only, and the flag registry (dokimi/cli_flags.py) makes --out the canonical output flag and bans -o, so an alias would fail the flag ratchet. The changelog already says --out FILE (b9f0980), and the docs match.

r.set_defaults(func=cmd_eval_redteam)
6 changes: 6 additions & 0 deletions dokimi/dokimi/eval/judge.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,3 +678,9 @@ def load_evaluations(input_path: Path) -> Dict[str, Any]:
def _get_timestamp() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).replace(tzinfo=None).isoformat() + "Z"


def judge_ask(model: str, provider: str) -> Callable[..., str]:
"""The ``Ask`` of the reference checks (one prompt in, the reply text out) backed by a ``Judge``'s model."""
llm = Judge(model=model, provider=provider).llm
return lambda prompt, draw=0, cache_if=None: llm.invoke(prompt, draw=draw, cache_if=cache_if).content
Loading
Loading