Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/fi/alk/providers/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ def _authored_tool(


def _redact(value: Any, key: str = "") -> Any:
refused: list[str] = []
redacted = _redact_in(value, key, refused)
if refused:
# Once per document, not per string: a crafted contract can hold half a million of them.
logger.warning("a URL was redacted whole: class=%s", refused[0])
return redacted


def _redact_in(value: Any, key: str, refused: list[str]) -> Any:
lowered = key.lower()
if any(marker in lowered for marker in _REDACTION_MARKERS):
return _REDACTED
Expand All @@ -242,18 +251,19 @@ def _redact(value: Any, key: str = "") -> Any:
return _REDACTED
if isinstance(value, Mapping):
return {
str(item_key): _redact(item, str(item_key))
str(item_key): _redact_in(item, str(item_key), refused)
for item_key, item in value.items()
}
if isinstance(value, list):
return [_redact(item, key) for item in value]
return [_redact_in(item, key, refused) for item in value]
if isinstance(value, str) and "://" in value:
try:
parsed = urlsplit(value)
except ValueError as refused:
except ValueError as error:
# A netloc urlsplit refuses may still hold credentials, so none of it is kept, and
# only the error's class is logged: its text repeats the netloc.
logger.warning("a URL was redacted whole: class=%s", type(refused).__name__)
if not refused:
refused.append(type(error).__name__)
return _REDACTED
if parsed.scheme and parsed.netloc:
hostname = parsed.hostname or ""
Expand Down
5 changes: 5 additions & 0 deletions tests/harness/test_agent_reading.py
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,11 @@ def test_the_longest_stop_leaves_room_for_the_document_inside_the_margin():
)
assert bounds == (10.0, 60.0, 20.0)
assert sum(bounds) == 90.0
# And the reading the platform runs waits after the interrupt for exactly that bound.
assert agent_reading.READ_AGENT.work.func is agent_reading._read
assert agent_reading.READ_AGENT.work.keywords == {
"interrupt_wait": agent_reading._INTERRUPT_WAIT_SECONDS
}
# Then the saved contract's document, whose framework scan can take over a minute on a
# crafted repository: at least 150 s of the margin stays for it.
assert sum(bounds) + 150.0 < agent_reading._HARD_LIMIT_MARGIN_SECONDS
Expand Down
28 changes: 27 additions & 1 deletion tests/providers/test_providers_agent_build.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import annotations

import logging
from typing import Any

import pytest

from fi.alk.harness.provider_import import _safe_profile_value
from fi.alk.providers.agent import _redact
from fi.alk.providers.agent import _redact, build_authored_agent

PLAIN_DOCUMENTS: list[Any] = [
{"name": "Support line", "voice": {"voiceId": "marissa", "speed": 1.0}},
Expand Down Expand Up @@ -55,3 +56,28 @@ def test_added_markers_headers_and_parameters() -> None:
"headers": "[redacted]",
"url": "https://example.com/x",
}


def test_many_refused_urls_in_one_document_log_one_line(
caplog: pytest.LogCaptureFixture,
) -> None:
# A full-width number sign NFKC-normalises to "#", and "[" opens an IPv6 host that never
# closes: urlsplit refuses both.
leaky = "https://admin:hunter2\uff03x@db.example/"
refused = [leaky, *(["a://["] * 1000)]
document = {
"agent": "support",
"modality": "chat",
"system_prompt_excerpt": leaky,
"tools": [{"name": "add", "args": ["k"], "arg_values": {"k": refused}}],
}

with caplog.at_level(logging.DEBUG, logger="fi.alk.providers.agent"):
agent = build_authored_agent(document)

assert agent["behaviour"][0]["instructions"] == "[redacted]"
assert agent["tools"][0]["parameters"]["properties"]["k"]["enum"] == ["[redacted]"] * 1001
[logged] = [record for record in caplog.records if record.name == "fi.alk.providers.agent"]
assert logged.getMessage() == "a URL was redacted whole: class=ValueError"
assert logged.exc_info is None
assert "hunter2" not in caplog.text
Loading