feat(plugins): per-source pattern attribution + explicit pre-screen rebuild proof

Two review items raised on #65449 (thanks @hansai-art):

1. Explicit test that post-module-load registration REBUILDS the
   _PREFIX_SUBSTRINGS pre-screen tuple — plugin patterns flow through
   the same fast path as built-ins, never around it. This was covered
   implicitly by the masking tests; now it is asserted directly.

2. Plugin patterns are now stored keyed by registration source, giving
   the #64229 lifecycle/ownership-ledger work a clean seam to drop one
   plugin's patterns on unload. No public removal API is added —
   additive-only stands; unload remains a host-owned lifecycle concern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F
This commit is contained in:
webdevtodayjason
2026-07-16 07:45:46 -05:00
committed by Teknium
parent 50f12e6ad8
commit 22002b1d3e
2 changed files with 35 additions and 4 deletions

View File

@@ -1274,10 +1274,19 @@ def _has_known_prefix_substring(text: str) -> bool:
# (``security.redact_secrets: false`` / HERMES_REDACT_SECRETS) applies to
# plugin patterns exactly as it does to built-ins.
_PLUGIN_PREFIX_PATTERNS: list = []
# Keyed by registration source (e.g. "plugin:my-plugin") so the plugin
# lifecycle/ownership-ledger work (#64229) has a clean seam to drop ONE
# plugin's patterns on unload. There is deliberately no public removal
# API — additive-only stands; unload is a host-owned lifecycle concern.
_PLUGIN_PREFIX_PATTERNS: dict = {}
_registry_lock = threading.Lock()
def _plugin_patterns() -> list:
"""All plugin-registered patterns in registration order."""
return [p for patterns in _PLUGIN_PREFIX_PATTERNS.values() for p in patterns]
def _rebuild_prefix_matcher() -> None:
"""Recompile the prefix alternation and pre-screen substrings.
@@ -1286,7 +1295,7 @@ def _rebuild_prefix_matcher() -> None:
under the GIL) propagates immediately to every caller.
"""
global _PREFIX_RE, _PREFIX_SUBSTRINGS
combined = _PREFIX_PATTERNS + _PLUGIN_PREFIX_PATTERNS
combined = _PREFIX_PATTERNS + _plugin_patterns()
_PREFIX_RE = re.compile(
r"(?<![A-Za-z0-9_-])(" + "|".join(combined) + r")(?![A-Za-z0-9_-])"
)
@@ -1365,14 +1374,14 @@ def register_redaction_patterns(patterns, source: str = "plugin") -> int:
source, pattern,
)
continue
if pattern in _PREFIX_PATTERNS or pattern in _PLUGIN_PREFIX_PATTERNS or pattern in accepted:
if pattern in _PREFIX_PATTERNS or pattern in _plugin_patterns() or pattern in accepted:
logger.debug("%s: redaction pattern %r already registered", source, pattern)
continue
accepted.append(pattern)
if accepted:
with _registry_lock:
_PLUGIN_PREFIX_PATTERNS.extend(accepted)
_PLUGIN_PREFIX_PATTERNS.setdefault(source, []).extend(accepted)
_rebuild_prefix_matcher()
logger.info(
"%s: registered %d redaction pattern(s)", source, len(accepted)

View File

@@ -55,6 +55,28 @@ def test_registered_pattern_masks_token():
assert "nvapi-" in out and "..." in out
def test_prescreen_tuple_rebuilt_not_bypassed():
# The cheap pre-screen gate (_has_known_prefix_substring) consults
# _PREFIX_SUBSTRINGS. Registration after module load must REBUILD that
# tuple so plugin patterns flow through the same fast path as built-ins
# — never around it.
assert "nvapi-" not in redact_mod._PREFIX_SUBSTRINGS
assert not redact_mod._has_known_prefix_substring(f"x {NVAPI_KEY}")
register_redaction_patterns([NVAPI_PATTERN], source="test")
assert "nvapi-" in redact_mod._PREFIX_SUBSTRINGS
assert redact_mod._has_known_prefix_substring(f"x {NVAPI_KEY}")
def test_patterns_attributed_per_source():
# Patterns are stored keyed by registration source — the seam the
# #64229 lifecycle/ledger path needs to drop one plugin's patterns on
# unload. No public removal API exists; additive-only stands.
register_redaction_patterns([NVAPI_PATTERN], source="plugin:alpha")
register_redaction_patterns([r"zk-[A-Za-z0-9]{24,}"], source="plugin:beta")
assert redact_mod._PLUGIN_PREFIX_PATTERNS["plugin:alpha"] == [NVAPI_PATTERN]
assert redact_mod._PLUGIN_PREFIX_PATTERNS["plugin:beta"] == [r"zk-[A-Za-z0-9]{24,}"]
def test_builtins_unaffected_by_registration():
register_redaction_patterns([NVAPI_PATTERN], source="test")
sk = "sk-proj-AbCdEf1234567890GhIjKl"