From fdd8b71f4e74181a1ea3663bf7558e106791db96 Mon Sep 17 00:00:00 2001 From: 0xallam Date: Mon, 25 May 2026 02:13:43 -0700 Subject: [PATCH] Stop web_search from leaking upstream details into tool results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failure messages were echoing the raw requests-exception text — for an empty query the model would see "API request failed: 400 Client Error: Bad Request for url: https://api.perplexity.ai/chat/completions" and learn the upstream URL, the HTTP status, and the literal word "API" none of which it has any use for or right to. Same pattern in every except branch: KeyError leaked internal field names, generic exceptions leaked library exception text, etc. Two fixes: - Pre-flight reject empty/whitespace queries so the trivial misuse case never hits the network at all and gets a "Query cannot be empty." result immediately. - Sanitize every failure path: split RequestException into HTTPError (4xx → "rejected the query — refine and retry", 5xx → "service unavailable"), Timeout, ConnectionError, response-shape (KeyError / IndexError / ValueError), and a generic catch-all. Each path returns a short actionable message and logs the full traceback via logger.exception so operator-side observability is preserved. The model sees no URLs, no status codes, no library exception text. While in here: the missing-API-key message keeps the env var name because that's operator-actionable, and the dead "results": [] field the failure paths used to carry is dropped (success path never had it either, so the shape was inconsistent). Co-Authored-By: Claude Opus 4.7 (1M context) --- strix/tools/web_search/tool.py | 56 ++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py index 9ca55d3..88c09f7 100644 --- a/strix/tools/web_search/tool.py +++ b/strix/tools/web_search/tool.py @@ -41,14 +41,19 @@ Structure your response to be comprehensive yet concise, emphasizing the most cr security implications and details.""" -def _do_search(query: str) -> dict[str, Any]: +def _do_search(query: str) -> dict[str, Any]: # noqa: PLR0911 - each error class needs its own sanitized return + if not query or not query.strip(): + return {"success": False, "message": "Query cannot be empty."} + api_key = load_settings().integrations.perplexity_api_key if not api_key: logger.warning("web_search invoked without PERPLEXITY_API_KEY configured") return { "success": False, - "message": "PERPLEXITY_API_KEY environment variable not set", - "results": [], + "message": ( + "Web search is not configured for this scan " + "(operator needs to set PERPLEXITY_API_KEY). Proceed without it." + ), } logger.info("web_search query (len=%d): %s", len(query), query[:120]) @@ -62,32 +67,57 @@ def _do_search(query: str) -> dict[str, Any]: ], } + # Internal details (upstream URL, HTTP status, library exception text) stay + # in the logs; the model only ever sees a short actionable category so it + # can decide whether to retry, refine, or work around the gap. try: response = requests.post(url, headers=headers, json=payload, timeout=300) response.raise_for_status() content = response.json()["choices"][0]["message"]["content"] except requests.exceptions.Timeout: logger.warning("web_search timed out") - return {"success": False, "message": "Request timed out", "results": []} - except requests.exceptions.RequestException as e: - logger.exception("web_search API request failed") - return {"success": False, "message": f"API request failed: {e!s}", "results": []} - except KeyError as e: + return { + "success": False, + "message": "Web search timed out. Try again or shorten the query.", + } + except requests.exceptions.HTTPError as exc: + status = exc.response.status_code if exc.response is not None else None + logger.exception("web_search HTTP error status=%s", status) + if status is not None and 400 <= status < 500: + return { + "success": False, + "message": ( + "Web search rejected the query. Refine it " + "(more specific, shorter, no unusual characters) and retry." + ), + } + return { + "success": False, + "message": "Web search service is unavailable. Try again later.", + } + except requests.exceptions.RequestException: + logger.exception("web_search network error") + return { + "success": False, + "message": "Web search network error. Try again later.", + } + except (KeyError, IndexError, ValueError): logger.exception("web_search response shape unexpected") return { "success": False, - "message": f"Unexpected API response format: missing {e!s}", - "results": [], + "message": "Web search returned an unexpected response. Try again.", } - except Exception as e: + except Exception: logger.exception("web_search failed") - return {"success": False, "message": f"Web search failed: {e!s}", "results": []} + return { + "success": False, + "message": "Web search failed unexpectedly.", + } else: return { "success": True, "query": query, "content": content, - "message": "Web search completed successfully", }