feat(tokens): add calibrate_perplexity_threshold for the OT.4 perplexity seam - #445
Conversation
…ity seam TokenChannelDetector accepts an opt-in perplexity_fn seam, but nothing in the tree could calibrate a perplexity_threshold from real data. Adds calibrate_perplexity_threshold(benign_scores, target_fpr), the nearest-rank empirical (1 - target_fpr) quantile of a benign score corpus. Fails closed on a target_fpr outside (0, 1) or fewer than 20 benign scores. Fixes DobermanCore#234
d085a85 to
f1b9b49
Compare
|
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:
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. |
…plexity_threshold A NaN in benign_scores sails through sorted() unnoticed; every downstream score >= threshold comparison against it is False, which silently disables the perplexity channel instead of raising like the two existing guards. Reject any non-finite sample the same way. Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
|
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. |
|
Merged, thanks @AmirF194! The follow-up commit that rejects non-finite scores is the part I like most, a NaN would have made every |
Pull Request
Slice
What this PR does
TokenChannelDetector(src/doberman/engine/detectors/token_channels.py) accepts an opt-inperplexity_fnseam for the statistical OOD token channel, but nothing in the tree could pick aperplexity_thresholdfrom real data. Issue #235 (the reference windowed scorer) names this asits blocker: "Blocked by #234 (the calibration helper lands first)".
Adds
doberman.tokens.calibrate_perplexity_threshold(benign_scores, target_fpr), returning thenearest-rank empirical
(1 - target_fpr)quantile of a benign score corpus: the smallest scoredsample 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 exceedtarget_fprby up to onesample (n=20, target 0.10 measures 0.15 in the worst case). Fails closed: raises
ValueErroron atarget_fproutside(0, 1), fewer than 20 benign scores, or any non-finite score, since none ofthose can honestly promise the requested false-positive rate.
Tests added (run in CI)
test_calibrate_perplexity_threshold_meets_target_fpr,..._ignores_input_order,..._rejects_bad_target_fpr,..._rejects_too_few_samples,..._handles_saturated_scores,..._rejects_non_finite_scores(
tests/unit/test_tokens_scanner.py), all with fixed, seed-free score arrays per your note onthe issue.
test_calibrated_threshold_escalates_gcg_suffix_and_passes_benign(
tests/unit/test_detector_token_channels.py): wires a calibrated threshold throughTokenChannelDetectorwith 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.py96%, 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.pyfail in my Docker verificationimage (
libtk8.6.somissing frompython:3.13-slim), identically on a cleanmaincheckout inthe same image; unrelated to this change and not introduced by it.
Public-release safety (doberman-core only)
Security checklist
target_fpror too little data raises rather than returning a threshold that looks calibrated but is notEdge cases covered / Deviations from plan / Risks introduced
threshold is always a value that actually occurred, at the cost of granularity no finer than
1 / len(benign_scores).TokenChannelDetectoror a default scorer here; Optional [perplexity] extra: reference windowed scorer for the statistical OOD channel (OT.4) #235 (the reference windowedscorer) is the natural next step and stays a separate PR.
Written with AI assistance (Claude Code); every command in the verification section above was
run this session before pushing.
Fixes #234