Deny and allow lists
Your detector reads your company name as a person and you want that name left alone. Your internal codenames go undetected and you want them replaced every time. Both are decisions about the detection set rather than about the detector. DetectionOverride is the stage that imposes them, with two detectors. The deny list (deny_list) holds what is always masked, and its detector's hits are forced into the set. The allow list (allow_list) holds what is always left in clear, and its detector's hits are dropped from the set.
The stage runs right after detection, before overlap resolution and linking. Its two lists therefore trump the detector's reading, and also a corrected set coming back from a human review. See Architecture for the full stage order.
1. Keep a value in clear with an allow list
Point a detector at the value, hand it to DetectionOverride as the allow list, and pass the override to the pipeline. What the allow list finds leaves the detection set, so the value reaches the model in clear.
import asyncio
from piighost.components.detector import ExactMatchDetector
from piighost.components.override import DetectionOverride
from piighost.pipeline import AnonymizationPipeline
detector = ExactMatchDetector({"Emma": "PERSON", "Acme": "ORG"})
allow_list = ExactMatchDetector({"Acme": "ORG"})
override = DetectionOverride(allow_list=allow_list)
pipeline = AnonymizationPipeline(detector, override=override)
async def main() -> None:
result = await pipeline.anonymize("Emma works at Acme.")
print(result.text)
asyncio.run(main())The output should be:
<<PERSON:1>> works at Acme.allow_list_strategy decides which detections an allow list hit takes down.
- Keep
AllowListStrategy.VALUE, the default, when the value must never be de-identified whatever the detector calls it. It clears every detection carrying the same text, whatever its case, position and label. The label you write beside the value therefore never has to match the one the primary detector emits. - Use
AllowListStrategy.EXACTwhen the label is the point, and both detectors read the value the same way. It clears a detection only when its span and its label both match the hit. - Use
AllowListStrategy.OVERLAPwhen a longer detection containing the value must go down too. It clears any detection whose span touches a span on the allow list, labels ignored.
The three modes on one text, with a detector that mislabels Acme as a person and reads Globex Ltd as one organization.
import asyncio
from piighost.components.detector import ExactMatchDetector
from piighost.components.override import AllowListStrategy, DetectionOverride
from piighost.pipeline import AnonymizationPipeline
def build_pipeline(strategy: AllowListStrategy) -> AnonymizationPipeline:
detector = ExactMatchDetector(
{"Emma": "PERSON", "Acme": "PERSON", "Globex Ltd": "ORG"}
)
allow_list = ExactMatchDetector({"Acme": "ORG", "Globex": "ORG"})
override = DetectionOverride(allow_list=allow_list, allow_list_strategy=strategy)
return AnonymizationPipeline(detector, override=override)
async def main() -> None:
text = "Emma works at Acme, formerly Globex Ltd."
for strategy in AllowListStrategy:
pipeline = build_pipeline(strategy)
result = await pipeline.anonymize(text)
print(strategy.value, "->", result.text)
asyncio.run(main())The output should be:
exact -> <<PERSON:1>> works at <<PERSON:2>>, formerly <<ORG:1>>.
value -> <<PERSON:1>> works at Acme, formerly <<ORG:1>>.
overlap -> <<PERSON:1>> works at Acme, formerly Globex Ltd.EXACT matched neither detection, for two reasons. The allow list says Acme is an organization, where the detector says a person. And the detector's span covers Globex Ltd, where the allow list covers Globex alone. VALUE compares whole values, so it cleared Acme and left Globex Ltd, whose text is not the one on the allow list. OVERLAP cleared both, because the span on the allow list sits inside the longer detection.
2. Force a missed value with a deny list
Point a detector at the pattern the primary detector misses, here a codename a regex describes exactly, and hand it over as the deny list. Its hits enter the detection set whatever the primary detector saw.
import asyncio
from piighost.components.detector import RegexDetector
from piighost.components.override import DetectionOverride
from piighost.pipeline import AnonymizationPipeline
detector = RegexDetector.from_catalog("catalog:piighost/generic")
deny_list = RegexDetector({"CODENAME": r"ACME-[A-Z]+"})
override = DetectionOverride(deny_list=deny_list)
pipeline = AnonymizationPipeline(detector, override=override)
async def main() -> None:
result = await pipeline.anonymize("Ship ACME-FALCON to alice@example.com.")
print(result.text)
asyncio.run(main())The output should be:
Ship <<CODENAME:1>> to <<EMAIL:1>>.A forced hit also replaces every detection it overlaps, so the deny list label wins over the primary reading. Use that to correct a label, not only to add a detection.
from piighost.components.detector import ExactMatchDetector
detector = ExactMatchDetector({"Emma": "PERSON", "Acme": "PERSON"})
deny_list = ExactMatchDetector({"Acme": "ORG"})
override = DetectionOverride(deny_list=deny_list)
pipeline = AnonymizationPipeline(detector, override=override)
async def main() -> None:
result = await pipeline.anonymize("Acme hired Emma.")
print(result.text)
asyncio.run(main())The output should be:
<<ORG:1>> hired <<PERSON:1>>.A forced value goes through linking and token assignment like any other detection, so the conversational pipeline stores it in memory and deanonymize restores it.
3. De-identify a value the assistant introduced
In a thread, a value the assistant wrote first stays in clear even when the deny list matches it. The model produced that value because it was useful in context, and it does not know the value is confidential. Replacing it would strip the model's world knowledge, and signal that this precise value is sensitive. deny_list_strategy decides who wins.
- Keep
DenyListStrategy.RESPECT_PROVENANCE, the default, to leave an assistant-introduced value in clear. The deny list still guarantees the value is detected, and the same value introduced by the user is still tokenized. - Use
DenyListStrategy.FORCEto tokenize a value on the deny list whoever wrote it first.
import asyncio
from piighost.components.detector import ExactMatchDetector
from piighost.components.override import DenyListStrategy, DetectionOverride
from piighost.conversation_memory import MessageRole
from piighost.pipeline import ThreadAnonymizationPipeline
def build_pipeline(strategy: DenyListStrategy) -> ThreadAnonymizationPipeline:
detector = ExactMatchDetector({})
deny_list = ExactMatchDetector({"Acme": "ORG"})
override = DetectionOverride(deny_list=deny_list, deny_list_strategy=strategy)
return ThreadAnonymizationPipeline(detector, override=override)
async def main() -> None:
for strategy in DenyListStrategy:
pipeline = build_pipeline(strategy)
assistant = await pipeline.anonymize(
"Acme rocks", thread_id="t1", role=MessageRole.ASSISTANT
)
user = await pipeline.anonymize("I love Acme", thread_id="t1")
print(strategy.value, "->", assistant.text, "|", user.text)
asyncio.run(main())The output should be:
respect_provenance -> Acme rocks | I love Acme
force -> <<ORG:1>> rocks | I love <<ORG:1>>4. Decide who wins when the two lists contradict
A value both lists match is a contradiction, and conflict_strategy names the winner.
- Keep
OverrideConflictStrategy.DENY_LIST_WINS, the default, to de-identify the contradicted value. The allow list applies to the primary detections first, then the deny list is forced in last. - Use
OverrideConflictStrategy.ALLOW_LIST_WINSto keep it in clear. The deny list is forced in first, then the allow list clears the result, forced hits included. - Use
OverrideConflictStrategy.RAISEto refuse the contradiction. A span on the deny list overlapping one on the allow list raisesConflictingOverrideErrorbefore either list is applied.
import asyncio
from piighost.components.detector import ExactMatchDetector
from piighost.components.override import DetectionOverride, OverrideConflictStrategy
from piighost.exceptions import ConflictingOverrideError
from piighost.pipeline import AnonymizationPipeline
def build_pipeline(strategy: OverrideConflictStrategy) -> AnonymizationPipeline:
detector = ExactMatchDetector({"Emma": "PERSON"})
deny_list = ExactMatchDetector({"Acme": "ORG"})
allow_list = ExactMatchDetector({"Acme": "ORG"})
override = DetectionOverride(
deny_list=deny_list,
allow_list=allow_list,
conflict_strategy=strategy,
)
return AnonymizationPipeline(detector, override=override)
async def main() -> None:
for strategy in OverrideConflictStrategy:
pipeline = build_pipeline(strategy)
try:
result = await pipeline.anonymize("Emma works at Acme.")
except ConflictingOverrideError as error:
print(strategy.value, "->", type(error).__name__, error)
else:
print(strategy.value, "->", result.text)
asyncio.run(main())The output should be:
deny_list_wins -> <<PERSON:1>> works at <<ORG:1>>.
allow_list_wins -> <<PERSON:1>> works at Acme.
raise -> ConflictingOverrideError Overrides contradict each other on 'Acme': a span on the deny list overlaps one on the allow list.ALLOW_LIST_WINS clears a forced hit according to the allow list strategy. With the default VALUE, a forced value is therefore cleared whatever label the deny list attached to it. Under EXACT the two lists have to agree on the label for the allow list to win.
5. Drive both lists from a config file
Both lists are detector configs, [override.deny_list] and [override.allow_list], and the three strategies are keys of [override]. The file below forces the codename and keeps a public mailbox in clear.
[detector]
type = "regex"
catalogs = ["catalog:piighost/generic"]
[override]
allow_list_strategy = "value"
[override.deny_list]
type = "regex"
patterns = { CODENAME = 'ACME-[A-Z]+' }
[override.allow_list]
type = "exact"
values = { "public@corp.com" = "EMAIL" }load_pipeline parses the file and builds the pipeline, both lists included.
import asyncio
from piighost.config import load_pipeline
pipeline = load_pipeline("pipeline.toml")
async def main() -> None:
result = await pipeline.anonymize(
"Mail public@corp.com or alice@example.com about ACME-FALCON."
)
print(result.text)
asyncio.run(main())The output should be:
Mail public@corp.com or <<EMAIL:1>> about <<CODENAME:1>>.For every key and every accepted value, see the TOML configuration.
See also
- Pre-built detectors for the detectors the two lists are built on.
- Pipeline reference for the
overrideparameter and the stage order. - Guard rails for the output check the allow list exempts a value from.
- TOML configuration for the
[override]keys. - Impose a deny list and an allow list, for the business rules of the two lists,
BR-LIST-01toBR-LIST-08.