Files
Cortex-Inara/cortex/tools/web.py
Scott Idem 334e7f0dea feat: role-based tool access, confirmation gates, and new orchestrator tools
- auth_utils: get_user_role() reads role from auth.json (admin|user, default user)
- manage_passwords: new `role` command to promote/demote users (admin-only by convention)
- tools/__init__: TOOL_ROLES map, CONFIRM_REQUIRED set, get_tools_for_role(),
  get_openai_tools_for_role() — both orchestrators now filter tools by caller's role
- tools/system: cortex_restart (detached subprocess, 5s delay), cortex_logs (admin-only)
- tools/web: http_fetch — direct URL fetch, distinct from web_search
- tools/files: file_list (directory listing), file_write (restricted paths, admin-only)
- tools/notify: nc_talk_send — proactive outbound via notification.py
- orchestrator_engine + openai_orchestrator: user_role param; CONFIRM_REQUIRED tools
  return a confirmation-request result instead of executing — loop breaks after Claude
  asks user to confirm in a follow-up message
- home/scott/auth.json: role set to admin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 19:23:53 -04:00

77 lines
2.3 KiB
Python

"""
Web tools — search (DuckDuckGo) and direct HTTP fetch.
"""
import asyncio
import logging
import httpx
from config import settings
logger = logging.getLogger(__name__)
async def search(query: str, max_results: int | None = None) -> str:
"""Search DuckDuckGo and return results as a formatted string.
Returns a markdown-formatted list of results: title, URL, and snippet.
The orchestrator includes this in the context it passes to Claude.
"""
n = min(max_results or settings.ddg_max_results, 10)
results = await asyncio.to_thread(_sync_search, query, n)
if not results:
return f"No results found for: {query}"
lines = [f"Search results for: **{query}**\n"]
for i, r in enumerate(results, 1):
lines.append(f"{i}. [{r['title']}]({r['href']})")
if r.get("body"):
lines.append(f" {r['body']}")
lines.append("")
return "\n".join(lines).strip()
def _sync_search(query: str, max_results: int) -> list[dict]:
"""Synchronous DuckDuckGo search — run via asyncio.to_thread."""
from ddgs import DDGS
kwargs = {}
if settings.ddg_api_key:
# Paid account — pass token for higher rate limits
kwargs["headers"] = {"Authorization": f"Bearer {settings.ddg_api_key}"}
try:
with DDGS(**kwargs) as ddgs:
return list(ddgs.text(query, max_results=max_results))
except Exception as e:
logger.warning("DuckDuckGo search error: %s", e)
return []
async def http_fetch(
url: str,
method: str = "GET",
body: str | None = None,
timeout: int = 15,
) -> str:
"""Fetch a URL directly and return the response body.
Unlike web_search, this hits a specific URL — useful for health checks,
API probing, JSON endpoints, webhook testing, etc.
Response body is capped at 8 KB.
"""
method = method.upper()
timeout = min(max(int(timeout), 1), 60)
try:
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
resp = await client.request(method, url, content=body)
body_text = resp.text[:8192]
return f"HTTP {resp.status_code} {resp.url}\n\n{body_text}"
except httpx.HTTPError as e:
return f"HTTP error: {e}"
except Exception as e:
logger.warning("http_fetch error for %s: %s", url, e)
return f"Error: {e}"