From 22002b1d3eb46f286442eb07d3c171e61bc65b5c Mon Sep 17 00:00:00 2001 From: webdevtodayjason Date: Thu, 16 Jul 2026 07:45:46 -0500 Subject: [PATCH] feat(plugins): per-source pattern attribution + explicit pre-screen rebuild proof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01PnMCvi2vXqfs996AjVeF2F --- agent/redact.py | 17 +++++++++++++---- tests/test_redaction_registry.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/agent/redact.py b/agent/redact.py index 2512243dc0..496bbdcbce 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -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"(? 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) diff --git a/tests/test_redaction_registry.py b/tests/test_redaction_registry.py index 04c4d39445..8203cbceec 100644 --- a/tests/test_redaction_registry.py +++ b/tests/test_redaction_registry.py @@ -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"