diff --git a/openviking/privacy/skill_placeholder.py b/openviking/privacy/skill_placeholder.py index bc2ffabe40..d0879d7437 100644 --- a/openviking/privacy/skill_placeholder.py +++ b/openviking/privacy/skill_placeholder.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0 """Placeholder helpers for skill privacy values.""" +import re from dataclasses import dataclass, field @@ -21,18 +22,6 @@ def _replace_structured_value(content: str, raw_value: str, placeholder: str) -> replacements = ( (f'"{raw_value}"', f'"{placeholder}"'), (f"'{raw_value}'", f"'{placeholder}'"), - (f": {raw_value}\n", f": {placeholder}\n"), - (f": {raw_value}\r\n", f": {placeholder}\r\n"), - (f":{raw_value}\n", f":{placeholder}\n"), - (f":{raw_value}\r\n", f":{placeholder}\r\n"), - (f": {raw_value}", f": {placeholder}"), - (f":{raw_value}", f":{placeholder}"), - (f"= {raw_value}\n", f"= {placeholder}\n"), - (f"= {raw_value}\r\n", f"= {placeholder}\r\n"), - (f"={raw_value}\n", f"={placeholder}\n"), - (f"={raw_value}\r\n", f"={placeholder}\r\n"), - (f"= {raw_value}", f"= {placeholder}"), - (f"={raw_value}", f"={placeholder}"), ) replaced = False @@ -40,7 +29,20 @@ def _replace_structured_value(content: str, raw_value: str, placeholder: str) -> if old in content: content = content.replace(old, new) replaced = True - return content, replaced + + bare_value_pattern = re.compile( + rf"(?P[:=])(?P[ \t]*){re.escape(raw_value)}" + r"(?P[ \t]*)(?=\r?$)", + re.MULTILINE, + ) + content, bare_replacement_count = bare_value_pattern.subn( + lambda match: ( + f"{match.group('separator')}{match.group('leading')}" + f"{placeholder}{match.group('trailing')}" + ), + content, + ) + return content, replaced or bare_replacement_count > 0 def placeholderize_skill_content_with_blocks( diff --git a/tests/server/test_privacy_config_service.py b/tests/server/test_privacy_config_service.py index e4b8012621..d228577d7e 100644 --- a/tests/server/test_privacy_config_service.py +++ b/tests/server/test_privacy_config_service.py @@ -280,3 +280,29 @@ def test_placeholderization_replaces_all_structured_occurrences_for_same_value() 'api_key: "{{ov_privacy:skill:multi-hit-skill:api_key}}"\n' "backup={{ov_privacy:skill:multi-hit-skill:api_key}}\n" ) + + +def test_placeholderization_preserves_crlf_spacing_and_regex_special_values(): + result = placeholderize_skill_content_with_blocks( + "token:\ta.b+c?[x] \r\ntext: a.b+c?[x] should stay here\r\n", + "regex-skill", + {"token": "a.b+c?[x]"}, + ) + + assert result.replaced_values == {"token": "a.b+c?[x]"} + assert result.sanitized_content == ( + "token:\t{{ov_privacy:skill:regex-skill:token}} \r\ntext: a.b+c?[x] should stay here\r\n" + ) + + +def test_placeholderization_preserves_quoted_value_matching(): + result = placeholderize_skill_content_with_blocks( + 'note: use "secret" in prose\n', + "quoted-skill", + {"api_key": "secret"}, + ) + + assert result.replaced_values == {"api_key": "secret"} + assert result.sanitized_content == ( + 'note: use "{{ov_privacy:skill:quoted-skill:api_key}}" in prose\n' + )