Stop web_search from leaking upstream details into tool results

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) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-05-25 02:13:43 -07:00
parent 4f62adf820
commit fdd8b71f4e
+43 -13
View File
@@ -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",
}