From 4df54e896a922c349296cd90988a46a88657d46a Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Fri, 18 Sep 2026 01:27:56 +0000 Subject: [PATCH] harden web artifact evidence routing --- src/turn_contract.py | 23 +++++++++++++++++++---- tests/test_turn_contract.py | 26 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/turn_contract.py b/src/turn_contract.py index bc3c12e5d..e3eea0a97 100644 --- a/src/turn_contract.py +++ b/src/turn_contract.py @@ -701,10 +701,13 @@ def selected_tools_for_request(message: str) -> frozenset[str] | None: explicitly_named_web.add("web_fetch") if ( re.search(r"(?:file://)?/(?:tmp_)?workspace(?:/|\b)", raw_text, re.I) - and re.search( - r"\b(?:build|create|edit|persist|produce|save|write)\b", - raw_text, - re.I, + and ( + re.search( + r"\b(?:build|create|edit|persist|produce|save|write)\b", + raw_text, + re.I, + ) + or re.search(r"\brequired\s+outputs?\b", raw_text, re.I) ) and re.search( r"\b(?:artifacts?|director(?:y|ies)|files?|outputs?|results?)\b|" @@ -2557,6 +2560,18 @@ def required_read_operation_for_request(message: str, history: Iterable = ()) -> normalized_text = _normalize_request_lead(message) text, maximum = _read_request_and_limit(message) rows = list(history) + if re.search( + r"\bwithout\s+(?:using|trusting|relying\s+on)\s+(?:my\s+)?memory\b|" + r"\b(?:do\s+not|don['’]?t|dont|never)\b[^.;\n]{0,80}\bfrom\s+memory(?:\s+alone)?\b|" + r"\b(?:do\s+not|don['’]?t|dont|never)\s+(?:use|trust|rely\s+on)\s+(?:my\s+)?memory\b", + text, + re.I, + ): + # These are source-grounding constraints, not requests to read the + # user's private Odysseus memory store. Long research/artifact jobs + # often also contain words such as "list" or "show", which must not + # convert the evidence constraint into a sealed personal-data read. + return None # A user can switch families in one conversation and then explicitly come # back using ordinary shorthand (including a one-edit typo): # ``back to emaol show 2 latest``. This is a complete inbox inventory diff --git a/tests/test_turn_contract.py b/tests/test_turn_contract.py index 190f5a9b5..55e4363e4 100644 --- a/tests/test_turn_contract.py +++ b/tests/test_turn_contract.py @@ -2287,6 +2287,32 @@ def test_negative_memory_evidence_rule_does_not_select_personal_memory(): "the fetched page, never memory. Sort every paper list and show the result." ) assert required_read_operation_for_request(message) is None + + +@pytest.mark.parametrize( + "evidence_rule", + [ + "extract every field without trusting memory", + "do not answer from memory alone", + "do not write the inventory from memory", + ], +) +def test_web_evidence_variants_do_not_select_personal_memory(evidence_rule): + message = ( + "Use web_search and web_fetch, then show an author list and write " + f"/tmp_workspace/results/report.md; {evidence_rule}." + ) + assert required_read_operation_for_request(message) is None + + +def test_explicit_web_required_outputs_preserve_workspace_tools(): + message = ( + "Run web_search and web_fetch first. Required outputs in " + "/tmp_workspace/results: report.jsonl and provenance.md." + ) + assert selected_tools_for_request(message) == frozenset( + {"web_search", "web_fetch", "read_file", "write_file", "edit_file", "python"} + ) assert requested_capabilities(message) == {"search_browser", "shell_files"}