diff --git a/src/turn_contract.py b/src/turn_contract.py index a4d161356..6898f80a0 100644 --- a/src/turn_contract.py +++ b/src/turn_contract.py @@ -676,6 +676,28 @@ def selected_tools_for_request(message: str) -> frozenset[str] | None: if inline_text_transformation(raw_text): return frozenset() text = _normalize_request_lead(message) + if re.search( + r"\bfirst\s+tool\s+call\s+(?:must|should|needs?\s+to)\s+be\s+inspect_media\b", + raw_text, + re.I, + ): + # A trusted user can prescribe the first native evidence operation. + # Keep the remainder of an explicit media-to-artifact workflow + # available without letting content nouns (for example musical + # "notes") route to an unrelated personal-data product. + tools = {"inspect_media"} + if ( + re.search(r"\b(?:create|write|save|build|produce)\b", raw_text, re.I) + and re.search( + r"(?:file://)?/workspace/[^\s`\"']+\.(?:csv|html?|json|md|svg|txt)\b", + raw_text, + re.I, + ) + ): + tools.update({"write_file", "read_file"}) + if re.search(r"\b(?:preview|render|open)\b[^.\n]{0,100}\b(?:page|html|browser)\b", raw_text, re.I): + tools.add("private_browser") + return frozenset(tools) explicitly_named = { name for name in ("manage_notes", "manage_calendar", "manage_tasks") diff --git a/tests/test_turn_contract.py b/tests/test_turn_contract.py index 3ec5319a4..8aaff211e 100644 --- a/tests/test_turn_contract.py +++ b/tests/test_turn_contract.py @@ -89,6 +89,31 @@ def test_explicit_online_lookup_requests_route_to_web_search(prompt): assert requested_capabilities(prompt) == frozenset({"search_browser"}) +def test_musical_notes_do_not_route_to_personal_notes_when_media_tool_is_explicit(): + prompt = ( + "Your first tool call must be inspect_media for /workspace/input/reference.png. " + "Study the supplied piano score image, then create /workspace/output.html. " + "Reproduce the score with noteheads and light each piano key while each note sounds. " + "Preview the page in a browser and fix visual or timing defects before finishing." + ) + + assert selected_tools_for_request(prompt) == frozenset({ + "inspect_media", "write_file", "read_file", "private_browser", + }) + capabilities = requested_capabilities(prompt) + assert "notes" not in capabilities + assert {"media_inspection", "shell_files", "search_browser"} <= capabilities + + +def test_musical_note_language_alone_never_selects_personal_notes(): + capabilities = requested_capabilities( + "Inspect the piano score image and identify each note, pitch, stave, clef, and notehead." + ) + + assert "notes" not in capabilities + assert "media_inspection" in capabilities + + def test_successfully_used_tool_stays_offered_when_next_turn_routes_elsewhere(): contract = resolve( {"notes"},