fix(guardrails): pin how the privaite hook handles malformed input

The typed rewrite changed three paths that used to crash or skip. A Responses item whose type is not a string is now scanned like any unknown item instead of raising on endswith, a request with metadata set to null now gets its restore map instead of a TypeError, and a dict-shaped response is now restored like an object one. Each one has a test now

It also put a privaite install missing PIIEngine on a TypeError, where the old import raised the install hint. That path is back on the hint and tested
This commit is contained in:
crp4222 2026-10-21 13:06:04 +02:00
parent 3486434407
commit 333d99e25b
2 changed files with 43 additions and 3 deletions

View file

@ -65,9 +65,11 @@ def _module(name: str) -> ModuleType:
def _factory(module: ModuleType, name: str) -> Callable[..., object]:
value: Final = getattr(module, name, None)
if not callable(value):
raise TypeError(f"{module.__name__}.{name} is missing. {INSTALL_HINT}")
return value
if callable(value):
return value
if hasattr(module, name):
raise TypeError(f"{module.__name__}.{name} is not callable. {INSTALL_HINT}")
raise RuntimeError(f"{module.__name__}.{name} is missing. {INSTALL_HINT}")
def _narrow(value: object, kind: type[_Narrowed]) -> _Narrowed:

View file

@ -1386,3 +1386,41 @@ async def test_streaming_tool_arguments_stay_valid_json_with_special_chars(monke
assert json.loads(tool_args) == {"name": _SPECIALS_ORIGINAL}
assert json.loads(fc_args) == {"name": _SPECIALS_ORIGINAL}
assert content == f"Hi {_SPECIALS_ORIGINAL}"
@pytest.mark.asyncio
async def test_pre_call_scans_a_responses_item_whose_type_is_not_a_string():
gr = _make_guardrail()
data = {"input": [{"type": 5, "output": "Email Marie Dupont"}]}
out = await gr.async_pre_call_hook(None, None, data, "aresponses")
assert out["input"][0]["output"] == "Email <PERSON_1>"
@pytest.mark.asyncio
async def test_pre_call_stashes_the_map_when_metadata_is_null():
gr = _make_guardrail()
data = {"messages": [{"role": "user", "content": "Email Marie Dupont"}], "metadata": None}
out = await gr.async_pre_call_hook(None, None, data, "acompletion")
assert out["metadata"]["privaite_map"] == {"<PERSON_1>": "Marie Dupont"}
@pytest.mark.asyncio
async def test_post_call_restores_a_dict_shaped_response():
gr = _make_guardrail()
response = {"choices": [{"message": {"content": "Sending to <PERSON_1>"}}]}
out = await gr.async_post_call_success_hook({"metadata": {"privaite_map": _FAKES}}, None, response)
assert out["choices"][0]["message"]["content"] == "Sending to Marie Dupont"
@pytest.mark.asyncio
async def test_engine_for_a_privaite_without_pii_engine_recommends_the_install(monkeypatch):
monkeypatch.delattr(sys.modules["privaite.pii.engine"], "PIIEngine")
with pytest.raises(RuntimeError, match=r"pip install 'privaite>=0\.7\.2'"):
await _make_guardrail()._engine_for(["en"])