fix(eval,detect): correct bench gold, gate geolocation, close URL bypass - #47
Open
lBroth wants to merge 1 commit into
Open
fix(eval,detect): correct bench gold, gate geolocation, close URL bypass#47lBroth wants to merge 1 commit into
lBroth wants to merge 1 commit into
Conversation
Supersedes #44, #45, #46. OOD-7 macro 0.7784 -> 0.8290. BENCH CORRECTNESS. `_map_ai4privacy_label` and the isotonic loader resolved unknown upstream labels through `dict.get()`, which returns None both for "deliberately excluded" and for "never seen". Gold was deleted with no signal: 56.4% on ai4privacy-300k-heldout, ~11% on the isotonic -heldout rows. Since macro_f1 skips zero-support classes, predictions on a deleted class cost nothing — the bug hid recall AND inflated precision. Re-running all 9 tools reorders 8 of 9 on ai4privacy-300k-heldout. All 25 recovered labels are aliases of keys already present; selection is by schema membership, not score. GEOLOCATION GATE. The schema defines private_geolocation as a coordinate; the zero-shot GLiNER head applies it to place names and postcodes (only 7.6% of emissions were coordinate-shaped). Every gold span of the class is a lat/lon pair, so a shape gate cannot remove a conforming true positive: -2317 false positives, +-0 true positives, precision 0.129 -> 0.902. Runs before dedupe so the competing private_address candidate survives the overlap (+83 true positives). URL ALLOWLIST BYPASS. #45's feature ships with its bypass closed. `core:url` is greedy, so PII glued to an allowlisted URL was swallowed into the span, the inner span dropped by containment regardless of score, and the outer span dropped by the allowlist — emitting AWS keys, GitHub PATs and Anthropic keys as plaintext behind a 20-character prefix. Now an allowlisted URL is dropped only when it carries no other PII span. Plus a multi-scheme guard and .hostname over .host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01463aDpX9uxUxNrrNzLkCbg
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #44, #45 and #46. #45's feature ships here with its redaction bypass closed; #44 and #46 are closed with the evidence on their own threads.
OOD-7 macro 0.7784 → 0.8290. Two independent halves, kept separate because only one is a product improvement.
nullpii-benchPart 1 — the bench was deleting gold, and the deletion was laundering precision
_map_ai4privacy_labeland the isotonic loader resolved unknown upstream labels throughdict.get(), which returnsNonefor "deliberately excluded" and for "never seen" alike. A label the map had never heard of was dropped with no signal: 56.4% of gold onai4privacy-300k-heldout(18,661 of 33,115 spans), ~11% on the isotonic-heldoutrows.Because
macro_f1skips classes with zero gold support, a prediction on a class whose gold had been deleted cost nothing at all. The bug was not only hiding recall, it was inflating precision — nullpii's 1,331 phone, 2,364 account_number, 1,625 geolocation, 839 passport, 676 driver-licence and 767 secret false positives on that one row were entirely unpenalised.All 25 recovered labels are aliases of keys already in the map.
_AI4PRIVACY_LABELSwas authored against the 400k release; 300k spells the same taxonomy differently (GIVENNAME1/LASTNAME1/TEL/BOD/PASSvsGIVENNAME/SURNAME/TELEPHONENUM/DOB/PASSWORD). 44 of the map's 67 keys were dead, which is why it looked comprehensive.Mapping and exclusion are now distinct, and anything in neither raises:
Selection is by schema membership, not by score.
LITECOINADDRESSis mapped even though it lowers our macro by 0.0013;AGE,USERAGENTandACCOUNTNAMEstay excluded even though mapping them would lower it further (−0.0054, −0.0177, −0.0046) — the exclusion list was checked symmetrically rather than being only the mappings that flatter us.This changes the ranking, not just the numbers. Re-running all 9 tools on the 5 affected rows reorders 8 of 9 on
ai4privacy-300k-heldout:piiranha+0.2347,gliner-pii-large-v1+0.1801,gliner-onnx-pii-fp32+0.1423,nullpii−0.0519,presidio−0.1746. Tools whose output vocabulary covers the full ai4privacy taxonomy were the ones the bug punished hardest.Also here:
packages/eval/scripts/ood_macro.pybecomes the single definition of the OOD-7 set (the root README said 7 datasets andpackages/eval/datasets/README.mdsaid 5 — a 0.0128 discrepancy that would silently break any regression gate), and the eval wheel gets a per-file licence allowlist instead of force-including the wholedatasets/directory.Part 2 —
private_geolocationwas labelling place namesThe schema defines the label as a coordinate: all three recognizers that own it match lat/lon literals and
core:geo-latlon-decimalrange-validates. The GLiNER head, prompted zero-shot and never trained on it, applies it to place names, regions and postcodes. Onai4privacy-300k-heldoutonly 7.6% of 1,625 emissions were coordinate-shaped; the rest wereHolter Straße,Altrip,Deutschland,Sachsen,52396.Every gold span of the class is a lat/lon pair, and so is every correct prediction, so a shape gate cannot remove a true positive that matches the schema's own definition. Measured: −2,317 false positives, ±0 true positives, recall unchanged to four decimals,
private_geolocationprecision 0.129 → 0.902.It runs before dedupe on purpose. A place name tagged geolocation is both a false positive and an unmatched
private_addressgold; filtering raw decoder output lets the competing candidate survive the overlap instead of losing it to a label it should never have won. That ordering alone recovers 83 true positives —private_ip+60,private_address+11,private_vehicle_id+7,private_mac+5.This is not only a bench artifact: today a user who sends
Deutschlandor a postcode gets a GPS-coordinate placeholder back.Part 3 — the public-URL allowlist, with its bypass closed
#45's goal is sound:
github.com/acme/repoin a system prompt is not PII and redacting it destroys signal the upstream model needs. As written it opened a general redaction bypass.core:urlmatches[^\s<>"]+, so PII glued to a URL is swallowed into the URL span;removeContainedSpansthen drops the inner span regardless of score, and dropping the outer span emitted the secret as plaintext. Verified end-to-end with the real model:mainx https://github.com/a,AKIAIOSFODNN7EXAMPLE yx https://github.com/a,ghp_ABCDEF… yx https://anthropic.com/x?key=sk-ant-api03-… yrepo https://github.com/acme/infra-gitops hereA 20-character allowlisted prefix disabled redaction for an arbitrary secret.
dropCleanPublicUrlSpansdrops an allowlisted URL span only when it carries no other PII span, running before dedupe while the nested spans still exist. Fail-safe by construction: a reference URL that embeds anything identifying reverts to whole-URL redaction. Note the naive fix — reordering unconditionally — is worse than either alternative: with realistic model output it shreds the URL into placeholders (repo https://{{PII_…}}.com/{{PII_…}}/infra-gitops), defeating the feature's own purpose.Two smaller fixes: a multi-scheme guard, because
core:urldoes not stop at,andhttps://github.com/foo,https://acme.io/internalwas allowlisted on the first host's authority; and.hostnameinstead of.host, which silently disabled the allowlist on any non-default port.Host matching itself needed no changes — 30+ attempted bypasses (
evil-github.com,github.com.attacker.net,github.com@evil.com, Cyrillic homographs, punycode,github.com。evil.io,acme-corp.github.io) all correctly redact.Verification
main): 10 for the geolocation gate, 10 for the nested-PII guard, plus feat(url): allowlist public-reference hosts from private_url output #45's own suite with one fixture corrected — it parked every span at offset 0, which under a containment-aware rule reads as "this URL has PII nested inside it".ai4privacy-400kprovably unaffected: 0 drops across all 406,896 rows, empty vocabulary intersection with the 15 new keys.presidio-syntheticmoves by exactly+0.0000— it carries noprivate_geolocationgold and none of its 89 URLs are allowlisted. That is the negative control.ood_macro.pyreproduces the published 0.7784 exactly, and exits non-zero if any cell is missing orCRASHED.Not in this PR
published-bench/is untouched. The 11 rows the gold fix did not affect still carry v0.3.0 numbers; publishing the full table needs them re-run for run-consistency (~6h).main,x https://github.com/users/john.doe@acme.com yyieldsprivate_url[2,44]andprivate_email[27,46]— IoU 0.39, below the dedupe threshold, so both survive and overwrite each other during vault substitution. The email span also ends past the end of the string.published-bench/:confusion.json(May 14) andmatrix.json(May 20) are from different runs and use different dataset keys, and the keynullpii-internal-benchno longer exists in the code (the registry key isnullpii-bench). Related:--datasets <unknown-key>prints "no datasets selected" and exits 0 instead of failing loud the way--tools <unknown>does.ai4privacy-300kis 100% English andai4privacy-300k-heldoutis 100% German — the upstream file is grouped by language, so that pair confounds memorisation with a locale switch.🤖 Generated with Claude Code
https://claude.ai/code/session_01463aDpX9uxUxNrrNzLkCbg