2424from ._types import ScanInput
2525from ._types import max_risk_level
2626
27+ import threading
28+
2729SCANNER_VERSION = "1.2.0"
2830
2931_custom_rules : list [SafetyRule ] = []
32+ _custom_rules_lock = threading .RLock ()
3033
3134
3235def register_custom_rule (rule : SafetyRule ) -> None :
3336 """Register a custom rule included in new scanners by default.
3437
3538 Existing SafetyScanner instances are unaffected. Registration is
36- idempotent by rule_id.
39+ idempotent by rule_id. Prefer passing ``rules=`` to SafetyScanner for
40+ process-local isolation; the global registry is a convenience for plugins.
3741 """
38- global _custom_rules
39- _custom_rules = [r for r in _custom_rules if r .rule_id != rule .rule_id ]
40- _custom_rules .append (rule )
42+ with _custom_rules_lock :
43+ global _custom_rules
44+ _custom_rules = [r for r in _custom_rules if r .rule_id != rule .rule_id ]
45+ _custom_rules .append (rule )
4146
4247
4348def register_rule (cls : type | None = None ):
@@ -63,16 +68,23 @@ def _decorate(rule_cls: type) -> type:
6368
6469def unregister_custom_rule (rule_id : str ) -> bool :
6570 """Remove a previously registered custom rule. Returns True if removed."""
66- global _custom_rules
67- before = len (_custom_rules )
68- _custom_rules = [r for r in _custom_rules if r .rule_id != rule_id ]
69- return len (_custom_rules ) < before
71+ with _custom_rules_lock :
72+ global _custom_rules
73+ before = len (_custom_rules )
74+ _custom_rules = [r for r in _custom_rules if r .rule_id != rule_id ]
75+ return len (_custom_rules ) < before
7076
7177
7278def clear_custom_rules () -> None :
7379 """Remove all custom rules from the registry."""
74- global _custom_rules
75- _custom_rules = []
80+ with _custom_rules_lock :
81+ global _custom_rules
82+ _custom_rules = []
83+
84+
85+ def _snapshot_custom_rules () -> list [SafetyRule ]:
86+ with _custom_rules_lock :
87+ return list (_custom_rules )
7688
7789
7890class SafetyScanner :
@@ -84,7 +96,8 @@ def __init__(
8496 rules : Optional [list [SafetyRule ]] = None ,
8597 ):
8698 self .policy = policy
87- self .rules = rules if rules is not None else default_rules () + list (_custom_rules )
99+ # Prefer explicit rules= for isolation; global registry is a snapshot.
100+ self .rules = rules if rules is not None else default_rules () + _snapshot_custom_rules ()
88101
89102 def scan (self , scan_input : ScanInput ) -> SafetyReport :
90103 """Scan *scan_input* and return a structured SafetyReport."""
0 commit comments