Originally created by: AmirF194
TokenChannelDetector (src/doberman/engine/detectors/token_channels.py) accepts an opt-in
perplexity_fn seam for the statistical OOD token channel, but nothing in the tree could pick a
perplexity_threshold from real data. Issue [#235] (the reference windowed scorer) names this as
its blocker: "Blocked by [#234] (the calibration helper lands first)".
Adds doberman.tokens.calibrate_perplexity_threshold(benign_scores, target_fpr), returning the
nearest-rank empirical (1 - target_fpr) quantile of a benign score corpus: the smallest scored
sample at or above that rank. Nearest-rank rather than interpolated, so the returned threshold is
a value that actually occurred in the corpus, at the cost of granularity no finer than
1 / len(benign_scores): the measured false-positive rate can exceed target_fpr by up to one
sample (n=20, target 0.10 measures 0.15 in the worst case). Fails closed: raises ValueError on a
target_fpr outside (0, 1), fewer than 20 benign scores, or any non-finite score, since none of
those can honestly promise the requested false-positive rate.
test_calibrate_perplexity_threshold_meets_target_fpr, ..._ignores_input_order,..._rejects_bad_target_fpr, ..._rejects_too_few_samples, ..._handles_saturated_scores,..._rejects_non_finite_scorestests/unit/test_tokens_scanner.py), all with fixed, seed-free score arrays per your note ontest_calibrated_threshold_escalates_gcg_suffix_and_passes_benigntests/unit/test_detector_token_channels.py): wires a calibrated threshold throughTokenChannelDetector with a stub scorer against a real GCG-style adversarial suffix fromtests/redteam/fixtures/gcg_suffixes.txt (AUTH) and an ordinary benign string (PASS).pytest -n auto --cov=doberman --cov-report=term-missing --cov-fail-under=80: 90.7% total,tokens.py 96%, no line this PR adds is in the missing set.ruff check ., ruff format --check ., lint-imports,python -m tools.parity.generate_parity --check: all clean.Not checked: the four real-Tk tests in test_gui_prompter.py fail in my Docker verification
image (libtk8.6.so missing from python:3.13-slim), identically on a clean main checkout in
the same image; unrelated to this change and not introduced by it.
target_fpr or too little data raises rather than returning a threshold that looks calibrated but is not1 / len(benign_scores).TokenChannelDetector or a default scorer here; [#235] (the reference windowedWritten with AI assistance (Claude Code); every command in the verification section above was
run this session before pushing.
Fixes [#234]
Originally posted by: fu351
Thanks @AmirF194, this is a strong first PR. The boundary tests hit the exact 19/20 and (0,1) edges, and reusing the real GCG fixture for the wiring test proves the seam against the actual attack class instead of a toy string. Two things before it merges:
benign_scoressails throughsorted()and can become the returned threshold, and downstream everyscore >= nancomparison is False, which silently disables the perplexity channel. Add a finiteness check that raisesValueErroron any non-finite sample, same fail-closed shape as your other two guards, plus a test with a NaN in the corpus.The red 3.11 leg wasn't yours, river 0.26.0 broke that leg repo-wide and the pin is merged as [#452]. The merge conflict it created on your branch is also mine to deal with, I'll land it from here once your two changes are in.
Related
Tickets:
#452Originally posted by: AmirF194
Both addressed and pushed (767d436).
Left the merge conflict from [#452] to you as you said. Ran the tokens test file plus ruff/format/lint-imports clean in Docker; the full suite is timing out in my sandbox for reasons unrelated to this change, so I have not re-confirmed the full coverage run this round.
Related
Tickets:
#452Ticket changed by: fu351
Originally posted by: fu351
Merged, thanks @AmirF194! The follow-up commit that rejects non-finite scores is the part I like most, a NaN would have made every
score >= thresholdcomparison false and quietly switched the perplexity channel off, and you caught that before I did. I broke the sort incalibrate_perplexity_thresholdlocally andtest_calibrate_perplexity_threshold_ignores_input_orderwent red straight away, which is the kind of test I want here. Welcome to Doberman! If you'd like the next piece of this seam, [#235] (level-6) is the windowed scorer that consumes the threshold you just calibrated, and [#143] (level-4) is a similar wire-an-interface job if you'd rather something smaller first. Feel free to join the Discord if you have questions: https://discord.gg/Sfy5XGNqtyRelated
Tickets: #143
Tickets: #235
Originally posted by: AmirF194
Thanks for the review and the merge. The NaN edge case was the sharper catch, that channel would have gone dark with no error. Appreciate the pointers to [#235] and [#143], I'll take a look.
Related
Tickets: #143
Tickets: #235