Repository navigation
Conversation
…stuffed answers The redteam command grades five synthetic answers against each question's grading block: a canary right answer (built from one literal check) must pass; a planted wrong answer, an empty answer, a hedge and a keyword-stuffed list must fail. An accepted attack or a rejected canary blocks the run (exit 1). Judge checks are unscored unless --judge is given. Nothing here changes a score. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Self-review found two probes that blocked a sound grading block: a negated check passes an empty, wrong or hedged answer, and the fixed hedge sentence is accepted by a check whose value occurs in it (for example a match on "one"). Both are now skipped; stuffed still runs for a negated check, and the hedge is skipped only on a clash. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…lisions Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A negate check passes a wrong, empty or hedged answer by design, but a
block that also has a positive check still rejects them. The red team
skipped all three for any negated check, so a gameable block such as
regex ".*" plus "must not say Lyon" reported pass with nothing probed.
- wrong, empty and hedge grade the block's non-negated checks alone. A
block of only negated checks gets none of them and is reported as
negated_only (row field, summary count, table line, schema).
- The hedge clash test also uses the grader's normalisation
(squad_normalize), so a value the grader finds in the hedge no longer
blames the grader for accepting it.
- A match at location end with ignore_punctuation false gets a canary
without a full stop, which the grader accepts.
Known limits: with a threshold, wrong/empty/hedge do not see the negated
checks' share of the score, so a block can accept the wrong answer and
still report pass. A choice value of several letters ("AB") is never
accepted by the grader, so its canary is rejected and blocks the run; that
is a grader issue for validate_grading.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
- wrong: the planted answer is picked per block from a few candidates so no positive literal value occurs in it; skipped only when none is clean. Stops valid graders (match "31", match "1", fuzzy "on") being blocked. - stuffed: not built for a negated exact, json or location-exact match, which the listed values never equal; built for one negated value so a block of a single negated check is probed with the forbidden value. - summary: unprobed count, in the table and redteam.v1 (schema, fixture). - docs: answer-formats.md and the module docstring match the code. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A negated match with mode all and the default location end gets the stuffed answer "Paris Lyon". That answer ends with one value only, so the all-match is false and the negation passes it: a correct grader was reported as accepting a stuffed answer and the run was blocked. Skip the probe for negated mode all, as the positive rule already does for mode all. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
CLI interface change detectedThis PR changes dokimi's user-facing CLI surface (commands and/or options): + added command `dokimi eval redteam`
+ argument `questions`
+ option `--help/-h`
+ option `--judge`
+ option `--judge-model`
+ option `--judge-provider`
+ option `--output/-o`Updated on every push; the |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #281 +/- ##
==========================================
+ Coverage 93.50% 93.56% +0.05%
==========================================
Files 273 275 +2
Lines 35569 35766 +197
Branches 6040 6069 +29
==========================================
+ Hits 33260 33465 +205
+ Misses 1492 1488 -4
+ Partials 817 813 -4
🚀 New features to boost your workflow:
|
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at e881ab5. The idea is useful and the plumbing is right, but the verdict is not yet trustworthy both ways: the stuffed probe blocks every match or fuzzy check with several aliases on a correct grader, and a threshold block whose negated check carries a wrong answer over the line still gets pass. Details are in the line comments.
What I ran, at e881ab5:
make ci-local --offline: 9,686 passed, 0 failed, 57 skipped; every CI row passed or was skipped for the network.run()on small grading blocks: an alias pair undermatch(anyandend) andfuzzyisblockedonstuffed;{threshold: 0.5, Paris + negated London}ispasswhile the realgrade()accepts the planted wrong and the empty answer; a negated-only block ispass; five judge questions without--judgeplus one match question ispasswith 5 of 6 unprobed.
Checked, no issues
- Wiring:
eval redteamis registered undereval, added to the judge-provider coverage test, and makes no model call without--judge; exit codes 1 (blocked, inconclusive) and 2 (bad input, unwritable-o) are tested. - Schema and changelog:
redteam.v1is new, listed inSCHEMA_VERSIONS, with a fixture; the fragment is under Added and says what the command does. No existing score, default or stored key changes. - New files: all carry the SPDX header.
antonwolfy
left a comment
There was a problem hiding this comment.
Review at e881ab5. The wiring, exit codes and the redteam.v1 schema hold up, but the verdict is not yet trustworthy: it blocks correct graders and passes weak ones. Two further problems are inline.
I agree with @vlad-perevezentsev's four points on the verdict (alias stuffing, threshold with a negated check, negated-only and unprobed runs reporting pass, wrong_for and numeric: true); I reproduced each. New evidence: {match, values: ["Paris", "Lyon"]} is blocked on stuffed, and {match, values: ["-7731.0"], numeric: true} is blocked on wrong.
What I ran, on a Linux review host at e881ab5:
make ci-local: 27 passed, 1 failed, 7 skipped; 9,739 tests passed, 4 skipped; changed-line coverage passed. The failure ispip-audit, which reports advisories onfilelockin the lock files; this PR does not touch them.- Reproductions for the code findings, with the LLM transport mocked.
- A 107-mutant run on
tests/test_redteam.py: 37 mutants pass the full suite; exit codes and the status verdict are pinned, the canary text and the clash logic are not. - Commits: all six carry
Signed-off-by.
Checked, no issues
- Wiring:
eval redteamis registered, a default run makes no model call, exit 2 on a missing, unreadable or non-JSON question file and on an unwritable-o. - Schema:
-ooutput validates againstredteam.v1; the kind is inSCHEMA_VERSIONS; the fixture and the changelog fragment are present;mkdocs build --strictpasses.
|
🔍 OpenCodeReview found 2 issue(s) in this PR. By severity: 1 high, 1 low
Review details
|
…the check itself The wrong candidate and the hedge skip were decided by a substring rule, so a numeric match that reads 7731 out of the planted answer blocked a correct grader, and a short value such as "one" dropped the hedge probe although an end match does not find it there. Each is now asked of the block's own positive literal checks. Thread: PRRT_kwDOVBg7g86xgzKG Thread: PRRT_kwDOVBg7g86xgzKN Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…hreshold included Dropping the negated checks from those three probes hid the one case where they matter: a threshold block whose negated check lifts a wrong answer to the line passed the red team while the real grader accepted it. Thread: PRRT_kwDOVBg7g86xgzJ2 Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…sive, not pass One probed question made the whole set pass while five of six judge questions were never scored, and a block of only negated checks counted as probed through its stuffed probe even though the real grader accepts any answer without the forbidden value. Both are now unprobed; any unprobed question makes the run inconclusive and the table names each of them. Thread: PRRT_kwDOVBg7g86xgzJ6 Thread: PRRT_kwDOVBg7g86xgzKC Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… the stuffed probe The bare list "4 four" for values ["4", "four"] ends with, begins with or contains an accepted value, so match and fuzzy checks with two or more aliases were blocked on a correct grader. A match at begin or end now gets the list between two decoys the check rejects; match at any, numeric match and fuzzy find the value anywhere, so no probe is built for them. Thread: PRRT_kwDOVBg7g86xgzJz Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
run looked each block up by question id, and the dict keeps one block per id, so two rows with the same id were both graded with the last block and a weak grader got pass. The blocks are now read from the question itself. Thread: PRRT_kwDOVBg7g86xhWCZ Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…artifact With --judge, a reader of the artifact could not tell whether the judge checks were scored by a model or which one; eval reference stores both. judge_model and judge_provider are required keys, null without --judge, while the schema is still new. Thread: PRRT_kwDOVBg7g86xgzKR Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…the grader's own helpers The red team had a copy of reference_checks._values and eval redteam a copy of the judge Ask builder of eval reference, so either could drift from the grader it tests. check_values is public now and judge_ask in eval.judge is the one builder both commands call. Thread: PRRT_kwDOVBg7g86xgzKW Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…written The table said "N more violations in the artifact" even without -o, when no artifact exists. Thread: PRRT_kwDOVBg7g86xgzKY Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… json value Dropping the location exact clause of canary_for, or replacing json.dumps with str, passed the whole file (43 tests) while the grader rejected the mutated canary. Each canary kind is now graded by the real grader; both mutations turn the new tests red. Thread: PRRT_kwDOVBg7g86xhWCh Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ted_only appears stuffed_for builds no stuffed answer for a negated match with mode all, which the table row did not say. format_table never prints the key negated_only: only the artifact has it, the table prints a line about the questions with only negated checks. Thread: PRRT_kwDOVBg7g86xlqTg Thread: PRRT_kwDOVBg7g86xlqTu Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…udge path Every command test set judge=False and none passed an ask, so dropping ask from run, ignoring --judge or swapping the model and provider all passed the file. A stub judge now scores the judge checks (rejecting and accepting), and the command test patches Judge and checks the arguments it receives. Each of the three mutations turns a new test red. Thread: PRRT_kwDOVBg7g86xlqT9 Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…e empty inputs Only run raising was tested; the except (OSError, ValueError) exit 2 path of the command never ran (narrowing it to OSError left the file green). Now a block with no values exits 2 with the problem and writes nothing, and run is pinned on an empty set, values [], a blank value, checks [] and grading None. Thread: PRRT_kwDOVBg7g86xlqT0 Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…mmand run Only the passing run was checked for a written artifact; writing it only for status pass left every test green although CI needs the artifact most for a blocked run. Both runs now pass -o and assert the file and its status. Thread: PRRT_kwDOVBg7g86xlqUH Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…th aliases The branch that lists the aliases of an exact, json or location-exact check without decoys had no test. Thread: PRRT_kwDOVBg7g86xgzJz Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Thread: PRRT_kwDOVBg7g86xgzJ6 Thread: PRRT_kwDOVBg7g86xgzKC Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… threads The review bodies and the bot comments restate or summarise the inline findings, each fixed in its own commit of this round. Thread: PRR_kwDOVBg7g88AAAABU4eT2Q Thread: PRR_kwDOVBg7g88AAAABU4wvyw Thread: PRR_kwDOVBg7g88AAAABU7EFzw Thread: IC_kwDOVBg7g88AAAABgrasmA Thread: IC_kwDOVBg7g88AAAABgrh3Cg Thread: IC_kwDOVBg7g88AAAABg12xOQ Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…tacks Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
The verdict findings it agrees with are fixed (see the replies in the vlad-perevezentsev threads) and its two own threads are answered: repeated ids 1b64508, canary tests f108f53. The |
|
Nothing to change here: the interface diff lists exactly what this PR adds, the command |
|
Covered by the tests of 27828e4, 0918eea and 1946f48: the report is for e881ab5. Locally, |
| except (OSError, ValueError) as e: | ||
| print(f"Error: {e}", file=sys.stderr) | ||
| sys.exit(2) |
There was a problem hiding this comment.
nit: a deeply nested questions file makes json.loads in load_questions raise RecursionError. It is not caught here, so the command exits with a traceback and code 1, which CI reads as a blocked run. Other commands (arms, catalog) catch RecursionError alongside OSError and ValueError for this reason.
Suggestion:
| except (OSError, ValueError) as e: | |
| print(f"Error: {e}", file=sys.stderr) | |
| sys.exit(2) | |
| except (OSError, ValueError, RecursionError) as e: |
There was a problem hiding this comment.
Fixed in 98ccb96. eval redteam now catches RecursionError next to OSError and ValueError when it reads the question set, so a set nested too deep to parse exits 2 with Error: ... on stderr instead of a traceback and exit 1. arms and catalog already catch it the same way.
Reproduced on the PR head with a 100,000-deep [[...]] file: the command died in json.loads with a RecursionError traceback and exit code 1. The new test test_command_exits_2_on_a_question_set_nested_too_deep_to_read fails on the old code and passes with the change.
| g = grade(grading, question, answer, ask) | ||
| out[name] = Probe(name, g.passed, _detail(g)) |
There was a problem hiding this comment.
must-fix: a probe is recorded as accepted even when part of the block was never scored. Grade.passed ignores checks with no score (counted drops them). Take a block of a judge check plus a negated literal check, for example [{judge: X}, {match: Lyon, negate: true}], in the default run with no --judge. The wrong, empty and hedge answers are graded on the negated check alone, which they pass by design. passed comes back True, so each is reported as a violation: the run is blocked and exits 1 for a block that is not gameable. The same happens when a positive regex times out. Mark the probe unscored when any check was unjudged or unrun and the grade says pass.
Suggestion:
| g = grade(grading, question, answer, ask) | |
| out[name] = Probe(name, g.passed, _detail(g)) | |
| g = grade(grading, question, answer, ask) | |
| # A pass built from only part of the block (judge checks without a judge, a timed-out regex) is not a measurement. | |
| passed = None if g.passed and (g.unjudged or g.unrun) else g.passed | |
| out[name] = Probe(name, passed, _detail(g)) |
There was a problem hiding this comment.
Blocking: Confirmed, and it has a second side that the suggested fix does not cover. I missed this in my first round.
The false block: [{judge}, {match: Lyon, negate: true}] without --judge is blocked on wrong, empty and hedge, because only the negated check is counted. This came in with 6b6aa2f, which now grades the whole block; before it the judge-only part gave None.
The false pass: {threshold: 0.5, checks: [{judge: names Paris}, {match: Paris}]} without --judge returns pass, exit 0, with the question counted as probed: the judge is dropped from the score, so wrong, empty and hedge score 0 and fail. With a judge that says yes to everything, the real grade() passes the planted wrong answer (0.5 >= 0.5), and run(ask=...) is blocked on all three. So the default run reads an unmeasured judge as a rejection.
Without a threshold a failed counted check is final (every check must pass), so only a pass is unsafe there. With a threshold neither result holds while a check has no score.
Suggestion:
g = grade(grading, question, answer, ask)
partial = g.unjudged or g.unrun
passed = None if partial and (g.passed or g.threshold is not None) else g.passedplus tests for both blocks above (the first should be unscored, the second unprobed and so inconclusive).
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at c68a1f1. Every point of my first round is fixed and I re-ran each repro. One problem is left, and it goes both ways: when part of a block has no score (a judge check without --judge, a timed-out regex), the probe is still read as a measurement. I added the false-pass side to @vchamarthi's thread.
What I ran, at c68a1f1:
make ci-local --offline: 10,553 passed, 0 failed, 57 skipped; every CI row passed or was skipped for the network.- The round-one repros: alias pairs under
match(any,begin,end) andfuzzy, andnumeric-7731.0, are nowpass.{threshold: 0.5, Paris + negated London}is nowblockedon wrong, empty and hedge. A negated-only block and the "5 judge + 1 match" set are nowinconclusive. A valueonekeeps its hedge probe. [judge, negated Lyon]without--judgeisblocked(a false block).{threshold: 0.5, [judge, match Paris]}without--judgeispass, butblockedwith a judge that says yes to everything.
Checked, no issues
- Stuffed answers: decoys are picked with the check's own
grade_check, so the decoys never hold an accepted value. Checks that read a value anywhere in the answer get no probe. - Repeated ids: each row is graded with its own block.
grading_by_idstill validates every row before any is graded. - Shared helpers:
check_valuesis a pure rename.judge_askbuilds the sameAskthateval referenceused. Nothing else imported_values, and the open PR #280 that editsreference_checks.pyadds no new call. - Artifact, docs and changelog:
judge_modelandjudge_providerare required and nullable inredteam.v1, which no release has written yet. The docs table and the fragment now describeinconclusiveand the stuffed rules as coded.
…th a traceback load_questions reads the file with json.loads, which raises RecursionError on deep nesting. eval redteam caught OSError and ValueError only, so the command died with a traceback and exit 1, which CI reads as a blocked run. arms and catalog already catch RecursionError for the same reason. Addresses review thread on dokimi/commands/redteam.py (PRRT_kwDOVBg7g86xtePO). Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… pass or a block
probe() took the grade's pass as the probe's verdict even when part of the block never
ran: a judge check with no judge model, or a regex past its timeout, is left out of
the score. Without a threshold a counted pass then reads as a pass that is not a
measurement, so `[{judge}, {match, negate}]` blocked wrong, empty and hedge answers
that the negated check passes by design. With a threshold the score over the counted
checks alone decides the probe, so `{threshold, [{judge}, {match}]}` passed the canary
and rejected the attacks with no judge at all, and `pass` exited 0.
Such a probe now has no verdict when the grade passed or a threshold is set, and the
run reports it inconclusive. A counted failure with no threshold stays a measured
rejection, since every counted check must pass.
Addresses review threads on dokimi/eval/redteam.py (PRRT_kwDOVBg7g86xtePe).
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 13 selected item(s). Review details
|
| if violations: | ||
| status = BLOCKED | ||
| else: | ||
| status = PASS if rows and not summary["unprobed"] else INCONCLUSIVE |
There was a problem hiding this comment.
This code returns inconclusive (exit 1) when any graded question is unmeasured. The PR description says the opposite in two places: inconclusive (no question was probed) and the known limit "Exit 0 when any one question is probed, even if others are unprobed". The module docstring, schema and changelog match the code. Update the PR description, since the repo guide asks that exit codes be described accurately. A mixed set, such as one negated-only block among normal ones, fails CI.
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…th and without a threshold Reverting the condition that unscores a probe with an unrun check turns both cases red: the negated match passes wrong, empty and hedge and the run is blocked, and a threshold scores the counted match alone and the run passes. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A json check with contains reads the first JSON in the answer, so the bare list "[1, 2] [3, 4]" starts with a listed value and the grader accepts it, which blocked a correct grading block. The list now follows two JSON decoys the check rejects, as match at begin or end does. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Summary
Part of #220.
eval redteam QUESTIONS.jsonchecks that each question'sgradingblock rejects the answers it should. It grades five synthetic answers per question:match,fuzzy,exact,choice,json). It must pass.any), or for one or more values of a negated check that is neithermode: allnor a whole-answer match (exact,json,location: exact).A
negatecheck does not switch the other attacks off: wrong, empty and hedge grade the block's non-negated checks. A block of only negated checks gets none of the three and is reported asnegated_only.Statuses:
pass,blocked(an attack was accepted or a canary was rejected),inconclusive(no question was probed). Exit 0 on pass, 1 on blocked or inconclusive, 2 on an unreadable question set or an unwritable-o.-owritesredteam.v1. Judge checks are unscored unless--judgeis given, so the default run makes no model call. No score changes.Evidence
match 31,match 1,fuzzy on) no longer blocks a correct grader; negated checks no longer silence the attacks on a positive check; a single negated value is stuffed; a negated whole-answer check is not stuffed; a check value in the hedge skips the hedge probe; the canary formatchat the end withignore_punctuation: falsehas no full stop; a run that probed nothing is inconclusive; an unwritable-oexits 2; the summary counts unprobed questions.FRESH-REVIEW-A2, diff e2147b1...04f2142; its quoted line numbers match 04f2142, not 71cc8e6) found three blockers, all fixed in 6e16c6b: a wrong answer that contained a check value blocked correct graders; stuffing a negatedexact,jsonorlocation: exactcheck blocked correct graders; a block with one negated value was never probed. In-session review found the negated-check silencing (fixed in 04f2142) and the hedge clash (fixed in 068abc8).make ci-local --offline): ruff, mypy (273 files), deptry, bandit, schema and registry checks, mkdocs and the CLI-surface job (which reports the addedeval redteam) pass. Pytest: 9735 passed, 5 skipped, 1 failed:test_container_agent.py::test_probe_reports_a_writable_docker_socket(AF_UNIX path too long). That test depends on the path length of the worktree: it failed in these/localdisk/napetrov/scratchworktrees and on a clean intel/main checkout (earlier run), and passed in the shorter base worktree.mode: allfix on e881ab5: a negatedmatchwithmode: alland the defaultlocation: endgot the stuffed answerParis Lyon. That answer ends with one value only, so the all-match is false and the negation passes it: the real grader accepted it and a correct grader was blocked. The stuffed probe is now skipped for negatedmode: all, as the positive rule already skipsmode: all. Red then green: the two new cases failed on 6e16c6b (2 failed, 41 passed) and pass on e881ab5 (43 passed).ruff checkandmypypass. A fresh-contextclaude -preview of6e16c6bb..e881ab57plusstuffed_forfound no blocker; it confirmed the grader rejects every stuffed answer that is still built, and its two minor points (the skip is wider than the one failing case; the docstrings overstated) are addressed.Merge Danger
eval redteamand new persisted formatredteam.v1(schema, fixture, registry entry). Both are new in this PR.eval redteamnow fails when a grader accepts a wrong answer, and when a question set has nothing gradeable (for example, judge checks only, without--judge).threshold: 0.5and[match Paris, match negate Lyon]accepts the planted wrong answer, and the red team reports pass. The docs state this.[regex ".*", match negate <planted text>]is blocked, but the real grader rejects the planted answer.mode: allcheck gets no stuffed probe at all, so a grader bug that accepts a stuffed answer for such a check is not caught. The skip is wider than the one case that false-blocks."AB"): the canaryANSWER: ABis rejected by the grader, so the run blocks. This is a grader issue invalidate_grading, not in the red team.--judge, judge checks call the model. The wrong answer is a fixed sentinel string, not one written for each question.🤖 Generated with Claude Code