From 61687726daada00d3e84777be9fad6121faa837f Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 13 Jun 2026 19:53:07 -0700 Subject: [PATCH] [haik]: refactor internal naming convention from leading-underscore (foo/FOO) to p/P prefix across anthropic_proxy, anthropic_to_openai, ssrf_guard, web tools, web_mcp_server, and auth router; clean up import aliases and local variable names in proxy handler; remove unused dashboard_layout module (models + router); remove orphaned healthcheck endpoint from anthropic proxy --- backend/apps/agents/proxy/anthropic_proxy.py | 117 +++++++++--------- .../apps/agents/proxy/anthropic_to_openai.py | 64 +++++----- backend/apps/agents/tools/ssrf_guard.py | 18 +-- backend/apps/agents/tools/web.py | 44 +++---- backend/apps/agents/web_mcp_server.py | 6 +- backend/apps/auth/router.py | 22 ++-- backend/apps/dashboard_layout/__init__.py | 0 .../apps/dashboard_layout/dashboard_layout.py | 58 --------- backend/apps/dashboard_layout/models.py | 27 ---- backend/apps/web/web.py | 2 +- 10 files changed, 135 insertions(+), 223 deletions(-) delete mode 100644 backend/apps/dashboard_layout/__init__.py delete mode 100644 backend/apps/dashboard_layout/dashboard_layout.py delete mode 100644 backend/apps/dashboard_layout/models.py diff --git a/backend/apps/agents/proxy/anthropic_proxy.py b/backend/apps/agents/proxy/anthropic_proxy.py index bf2fde75..d510ea93 100644 --- a/backend/apps/agents/proxy/anthropic_proxy.py +++ b/backend/apps/agents/proxy/anthropic_proxy.py @@ -21,7 +21,7 @@ async def anthropic_proxy_lifespan(): anthropic_proxy = SubApp("anthropic-proxy", anthropic_proxy_lifespan) -_CLAUDE_MODEL_PREFIXES = ( +P_CLAUDE_MODEL_PREFIXES = ( "claude-", "claude/", "sonnet", @@ -30,13 +30,13 @@ _CLAUDE_MODEL_PREFIXES = ( "cc/", ) -_GEMINI_MODEL_PREFIXES = ("gemini/", "gc/", "ag/") +P_GEMINI_MODEL_PREFIXES = ("gemini/", "gc/", "ag/") # Own-key Gemini ("gemini-3-flash-api" etc.) skips the gemini/ prefix; match bare names so $schema scrub still fires. -_GEMINI_BARE_MODEL_PATTERNS = ("gemini-",) +P_GEMINI_BARE_MODEL_PATTERNS = ("gemini-",) # Keys 9Router 0.3.60 misses that Gemini's function_declarations validator 400s on. Each was caught in prod. -_GEMINI_FORBIDDEN_SCHEMA_KEYS = { +P_GEMINI_FORBIDDEN_SCHEMA_KEYS = { "$schema", "$id", "$ref", @@ -59,27 +59,27 @@ _GEMINI_FORBIDDEN_SCHEMA_KEYS = { } -def _scrub_gemini_schema(node): +def p_scrub_gemini_schema(node): """Recursive in-place strip of Gemini-rejected JSON Schema fields.""" if isinstance(node, dict): for k in list(node.keys()): - if k in _GEMINI_FORBIDDEN_SCHEMA_KEYS: + if k in P_GEMINI_FORBIDDEN_SCHEMA_KEYS: node.pop(k, None) continue - node[k] = _scrub_gemini_schema(node[k]) + node[k] = p_scrub_gemini_schema(node[k]) return node if isinstance(node, list): for i, v in enumerate(node): - node[i] = _scrub_gemini_schema(v) + node[i] = p_scrub_gemini_schema(v) return node return node # GPT-5.x rejects max_tokens; needs max_completion_tokens. Anthropic-format wire still emits max_tokens; we rename on the way out. -_OPENAI_MAX_COMPLETION_TOKENS_MODELS = ("gpt-5",) +P_OPENAI_MAX_COMPLETION_TOKENS_MODELS = ("gpt-5",) -def _is_openai_max_completion_tokens_model(model: str) -> bool: +def p_is_openai_max_completion_tokens_model(model: str) -> bool: """Match every shape a GPT-5 name might arrive in (bare, api-suffixed, openai/-prefixed, cx/-routed).""" m = (model or "").strip().lower() if not m: @@ -88,10 +88,10 @@ def _is_openai_max_completion_tokens_model(model: str) -> bool: if m.startswith(prefix): m = m[len(prefix):] break - return any(m.startswith(p) for p in _OPENAI_MAX_COMPLETION_TOKENS_MODELS) + return any(m.startswith(p) for p in P_OPENAI_MAX_COMPLETION_TOKENS_MODELS) -def _rewrite_document_to_openai_file(parsed: dict) -> None: +def p_rewrite_document_to_openai_file(parsed: dict) -> None: """In-place: rewrite Anthropic base64 `image` blocks to OpenAI `image_url` (PDFs go via OpenRouter).""" msgs = parsed.get("messages") if isinstance(parsed, dict) else None if not isinstance(msgs, list): @@ -131,7 +131,7 @@ def _rewrite_document_to_openai_file(parsed: dict) -> None: } -def _scrub_request_for_openai_gpt5(body: bytes) -> bytes: +def p_scrub_request_for_openai_gpt5(body: bytes) -> bytes: """Rename max_tokens→max_completion_tokens for GPT-5 AND rewrite any Anthropic document blocks to OpenAI type:file shape so PDFs flow natively on GPT-5.x vision models. Bytes in/out; never raises.""" @@ -152,7 +152,7 @@ def _scrub_request_for_openai_gpt5(body: bytes) -> bytes: mutated = True try: before = json.dumps(parsed.get("messages"), sort_keys=True) if "messages" in parsed else "" - _rewrite_document_to_openai_file(parsed) + p_rewrite_document_to_openai_file(parsed) after = json.dumps(parsed.get("messages"), sort_keys=True) if "messages" in parsed else "" if before != after: mutated = True @@ -161,7 +161,7 @@ def _scrub_request_for_openai_gpt5(body: bytes) -> bytes: return json.dumps(parsed).encode("utf-8") if mutated else body -def _rewrite_document_to_image(parsed: dict) -> None: +def p_rewrite_document_to_image(parsed: dict) -> None: """In-place: rewrite Anthropic `document` (PDF) AND `image` content blocks → OpenAI `image_url` shape with a `data:` URL. Critical fix for 9router 0.3.60 which **only translates `image_url` blocks** to @@ -207,15 +207,15 @@ def _rewrite_document_to_image(parsed: dict) -> None: } -_OPENROUTER_MODEL_PREFIXES = ("openrouter/", "or:") +P_OPENROUTER_MODEL_PREFIXES = ("openrouter/", "or:") -def _is_openrouter_model(model: str) -> bool: +def p_is_openrouter_model(model: str) -> bool: m = (model or "").strip().lower() - return any(m.startswith(p) for p in _OPENROUTER_MODEL_PREFIXES) + return any(m.startswith(p) for p in P_OPENROUTER_MODEL_PREFIXES) -def _inject_openrouter_file_parser(body: bytes) -> bytes: +def p_inject_openrouter_file_parser(body: bytes) -> bytes: """When the request has document blocks AND is bound for OpenRouter, inject the file-parser plugin so OR's universal PDF support kicks in on any model (free models get pdf-text engine; native PDF models can @@ -255,7 +255,7 @@ def _inject_openrouter_file_parser(body: bytes) -> bytes: return json.dumps(parsed).encode("utf-8") -def _scrub_request_for_gemini(body: bytes) -> bytes: +def p_scrub_request_for_gemini(body: bytes) -> bytes: """Strip Gemini-incompatible schema keys from request tools AND rewrite Anthropic document blocks to image-shape so 9router's inline_data translator picks them up. Bytes-in/out, never raises.""" @@ -271,19 +271,19 @@ def _scrub_request_for_gemini(body: bytes) -> bytes: if not isinstance(t, dict): continue if isinstance(t.get("input_schema"), (dict, list)): - _scrub_gemini_schema(t["input_schema"]) + p_scrub_gemini_schema(t["input_schema"]) if isinstance(t.get("parameters"), (dict, list)): - _scrub_gemini_schema(t["parameters"]) + p_scrub_gemini_schema(t["parameters"]) try: if isinstance(parsed, dict): - _rewrite_document_to_image(parsed) + p_rewrite_document_to_image(parsed) except Exception: pass return json.dumps(parsed).encode("utf-8") # Hop-by-hop headers or auth we replace with the upstream-specific value. -_HOP_HEADERS = { +P_HOP_HEADERS = { "host", "content-length", "authorization", @@ -299,22 +299,22 @@ _HOP_HEADERS = { } -def _is_claude_model(model: str) -> bool: +def p_is_claude_model(model: str) -> bool: m = (model or "").strip().lower() - return m.startswith(_CLAUDE_MODEL_PREFIXES) + return m.startswith(P_CLAUDE_MODEL_PREFIXES) -def _is_gemini_model(model: str) -> bool: +def p_is_gemini_model(model: str) -> bool: m = (model or "").strip().lower() - if m.startswith(_GEMINI_MODEL_PREFIXES): + if m.startswith(P_GEMINI_MODEL_PREFIXES): return True # Bare-name match for own-key Gemini; excludes anthropic-routed gemini (those carry "/"). if "/" in m: return False - return any(m.startswith(p) for p in _GEMINI_BARE_MODEL_PATTERNS) + return any(m.startswith(p) for p in P_GEMINI_BARE_MODEL_PATTERNS) -def _pick_upstream(model: str) -> tuple[str, dict[str, str]]: +def p_pick_upstream(model: str) -> tuple[str, dict[str, str]]: """Return (base_url_without_v1, auth_headers) for this model. Routing for Claude-family models: @@ -326,7 +326,7 @@ def _pick_upstream(model: str) -> tuple[str, dict[str, str]]: from backend.apps.settings.settings import load_settings s = load_settings() - if _is_claude_model(model): + if p_is_claude_model(model): if getattr(s, "connection_mode", "own_key") == "openswarm-pro": bearer = getattr(s, "openswarm_bearer_token", "") or "" proxy = (getattr(s, "openswarm_proxy_url", "") or "https://api.openswarm.com").rstrip("/") @@ -352,9 +352,6 @@ def _pick_upstream(model: str) -> tuple[str, dict[str, str]]: methods=["GET", "HEAD", "OPTIONS"], include_in_schema=False, ) -async def _healthcheck(): - """CLI healthchecks the proxy root; return 200 so it doesn't 404.""" - return {"ok": True} @anthropic_proxy.router.api_route( @@ -382,46 +379,46 @@ async def proxy(rest: str, request: Request): parsed_for_bypass = None if isinstance(parsed_for_bypass, dict): from backend.apps.agents.proxy.anthropic_to_openai import ( - should_bypass_9router as _should_bypass_oai, - should_bypass_9router_for_openrouter as _should_bypass_or, - forward_to_openai as _forward_oai, - forward_to_openrouter as _forward_or, + should_bypass_9router, + should_bypass_9router_for_openrouter, + forward_to_openai, + forward_to_openrouter, ) - from backend.apps.settings.settings import load_settings as _load - _s = _load() - if _is_openai_max_completion_tokens_model(model): - _oak = (getattr(_s, "openai_api_key", "") or "").strip() - if _should_bypass_oai(parsed_for_bypass, _oak): - status, body_stream, hdrs = await _forward_oai( - parsed_for_bypass, _oak, + from backend.apps.settings.settings import load_settings + s = load_settings() + if p_is_openai_max_completion_tokens_model(model): + oak = (getattr(s, "openai_api_key", "") or "").strip() + if should_bypass_9router(parsed_for_bypass, oak): + status, body_stream, hdrs = await forward_to_openai( + parsed_for_bypass, oak, ) return StreamingResponse( body_stream, status_code=status, headers=hdrs, media_type=hdrs.get("content-type", "text/event-stream"), ) - if _is_openrouter_model(model): - _ork = (getattr(_s, "openrouter_api_key", "") or "").strip() - if _should_bypass_or(parsed_for_bypass, _ork): - status, body_stream, hdrs = await _forward_or( - parsed_for_bypass, _ork, + if p_is_openrouter_model(model): + ork = (getattr(s, "openrouter_api_key", "") or "").strip() + if should_bypass_9router_for_openrouter(parsed_for_bypass, ork): + status, body_stream, hdrs = await forward_to_openrouter( + parsed_for_bypass, ork, ) return StreamingResponse( body_stream, status_code=status, headers=hdrs, media_type=hdrs.get("content-type", "text/event-stream"), ) - if _is_gemini_model(model): - body = _scrub_request_for_gemini(body) - if _is_openai_max_completion_tokens_model(model): - body = _scrub_request_for_openai_gpt5(body) - if _is_openrouter_model(model): - body = _inject_openrouter_file_parser(body) + if p_is_gemini_model(model): + body = p_scrub_request_for_gemini(body) + if p_is_openai_max_completion_tokens_model(model): + body = p_scrub_request_for_openai_gpt5(body) + if p_is_openrouter_model(model): + body = p_inject_openrouter_file_parser(body) - base_url, auth_headers = _pick_upstream(model) + base_url, auth_headers = p_pick_upstream(model) forward_headers: dict[str, str] = {} for k, v in request.headers.items(): - if k.lower() in _HOP_HEADERS: + if k.lower() in P_HOP_HEADERS: continue # CLI carries our install token as x-api-key; never forward (leak + shadows real upstream auth). if k.lower() == "x-api-key": @@ -459,7 +456,7 @@ async def proxy(rest: str, request: Request): streamer(), status_code=upstream.status_code, headers={k: v for k, v in upstream.headers.items() - if k.lower() not in _HOP_HEADERS}, + if k.lower() not in P_HOP_HEADERS}, media_type=upstream.headers.get("content-type", "text/event-stream"), ) else: @@ -471,7 +468,7 @@ async def proxy(rest: str, request: Request): return JSONResponse( content=r.json() if r.headers.get("content-type", "").startswith("application/json") else {"raw": r.text}, status_code=r.status_code, - headers={k: v for k, v in r.headers.items() if k.lower() not in _HOP_HEADERS}, + headers={k: v for k, v in r.headers.items() if k.lower() not in P_HOP_HEADERS}, ) except httpx.TimeoutException: return JSONResponse({"error": "upstream timeout"}, status_code=504) diff --git a/backend/apps/agents/proxy/anthropic_to_openai.py b/backend/apps/agents/proxy/anthropic_to_openai.py index b92bf350..3f3c0c7b 100644 --- a/backend/apps/agents/proxy/anthropic_to_openai.py +++ b/backend/apps/agents/proxy/anthropic_to_openai.py @@ -26,8 +26,8 @@ import httpx logger = logging.getLogger(__name__) -_OPENAI_UPSTREAM = "https://api.openai.com/v1" -_OPENROUTER_UPSTREAM = "https://openrouter.ai/api/v1" +P_OPENAI_UPSTREAM = "https://api.openai.com/v1" +P_OPENROUTER_UPSTREAM = "https://openrouter.ai/api/v1" # Concurrency cap for bypass-route requests. Each in-flight request holds # the base64'd PDF (raw_bytes * 1.33) in memory across httpx's request @@ -37,18 +37,18 @@ _OPENROUTER_UPSTREAM = "https://openrouter.ai/api/v1" # concurrent probes. Cap at 2 so a Mehmet-style multi-PDF attach in one # session can't take the whole backend down. Requests above the cap # queue rather than fail. -_BYPASS_CONCURRENCY = 2 -_bypass_sema = asyncio.Semaphore(_BYPASS_CONCURRENCY) +P_BYPASS_CONCURRENCY = 2 +p_bypass_sema = asyncio.Semaphore(P_BYPASS_CONCURRENCY) # Hard per-request body size ceiling. Anthropic API caps at 32MB, # OpenAI Chat Completions at 50MB, OpenRouter at whatever underlying # model accepts. We refuse anything over 40MB raw (≈53MB base64) before # we even build the request body, so a malicious or accidental huge # attach never reaches the in-memory pipeline. -_BYPASS_MAX_RAW_BYTES = 40 * 1024 * 1024 +P_BYPASS_MAX_RAW_BYTES = 40 * 1024 * 1024 -def _has_document_block(parsed: dict) -> bool: +def p_has_document_block(parsed: dict) -> bool: msgs = parsed.get("messages") if not isinstance(msgs, list): return False @@ -75,7 +75,7 @@ def should_bypass_9router(parsed: dict, api_key: str | None) -> bool: return False if parsed.get("tools"): return False - return _has_document_block(parsed) + return p_has_document_block(parsed) def should_bypass_9router_for_openrouter(parsed: dict, api_key: str | None) -> bool: @@ -90,10 +90,10 @@ def should_bypass_9router_for_openrouter(parsed: dict, api_key: str | None) -> b return False if parsed.get("tools"): return False - return _has_document_block(parsed) + return p_has_document_block(parsed) -def _content_blocks_to_openai(content) -> list[dict]: +def p_content_blocks_to_openai(content) -> list[dict]: """Convert Anthropic content blocks → OpenAI Chat Completions parts.""" if isinstance(content, str): return [{"type": "text", "text": content}] @@ -134,7 +134,7 @@ def _content_blocks_to_openai(content) -> list[dict]: return out -def translate_request(parsed: dict) -> dict: +def p_translate_request(parsed: dict) -> dict: """Anthropic Messages request → OpenAI Chat Completions request.""" model = parsed.get("model") or "" if "/" in model: @@ -160,7 +160,7 @@ def translate_request(parsed: dict) -> dict: role = m.get("role") if role not in ("user", "assistant"): continue - msgs_out.append({"role": role, "content": _content_blocks_to_openai(m.get("content"))}) + msgs_out.append({"role": role, "content": p_content_blocks_to_openai(m.get("content"))}) openai_body["messages"] = msgs_out mt = parsed.get("max_tokens") @@ -176,12 +176,12 @@ def translate_request(parsed: dict) -> dict: return openai_body -def _sse_event(event: str, data: dict) -> bytes: +def p_sse_event(event: str, data: dict) -> bytes: """Encode an Anthropic-format SSE event.""" return f"event: {event}\ndata: {json.dumps(data)}\n\n".encode("utf-8") -async def _translate_response_stream( +async def p_translate_response_stream( upstream: httpx.Response, model: str, ) -> AsyncIterator[bytes]: """Convert OpenAI Chat Completions SSE → Anthropic Messages SSE. @@ -226,7 +226,7 @@ async def _translate_response_stream( if not started: usage = (ev.get("usage") or {}) input_tokens = int(usage.get("prompt_tokens") or 0) - yield _sse_event("message_start", { + yield p_sse_event("message_start", { "type": "message_start", "message": { "id": msg_id, @@ -255,13 +255,13 @@ async def _translate_response_stream( delta_text = delta.get("content") if isinstance(delta_text, str) and delta_text: if not block_opened: - yield _sse_event("content_block_start", { + yield p_sse_event("content_block_start", { "type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}, }) block_opened = True - yield _sse_event("content_block_delta", { + yield p_sse_event("content_block_delta", { "type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": delta_text}, @@ -277,15 +277,15 @@ async def _translate_response_stream( finally: if started: if block_opened: - yield _sse_event("content_block_stop", { + yield p_sse_event("content_block_stop", { "type": "content_block_stop", "index": 0, }) - yield _sse_event("message_delta", { + yield p_sse_event("message_delta", { "type": "message_delta", "delta": {"stop_reason": stop_reason, "stop_sequence": None}, "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens}, }) - yield _sse_event("message_stop", {"type": "message_stop"}) + yield p_sse_event("message_stop", {"type": "message_stop"}) async def forward_to_openai( @@ -293,8 +293,8 @@ async def forward_to_openai( ) -> tuple[int, AsyncIterator[bytes], dict[str, str]]: """Translate + forward an Anthropic request to OpenAI Chat Completions. Returns (status, body_stream, response_headers).""" - openai_body = translate_request(parsed) - return await _forward(openai_body, api_key, f"{_OPENAI_UPSTREAM}/chat/completions") + openai_body = p_translate_request(parsed) + return await p_forward(openai_body, api_key, f"{P_OPENAI_UPSTREAM}/chat/completions") async def forward_to_openrouter( @@ -302,7 +302,7 @@ async def forward_to_openrouter( ) -> tuple[int, AsyncIterator[bytes], dict[str, str]]: """Translate + forward to OpenRouter, injecting the file-parser plugin so any OR model parses the attached PDFs server-side.""" - openai_body = translate_request(parsed) + openai_body = p_translate_request(parsed) model = (parsed.get("model") or "").lower() bare = model for prefix in ("openrouter/", "or:"): @@ -311,10 +311,10 @@ async def forward_to_openrouter( break openai_body["model"] = bare openai_body["plugins"] = [{"id": "file-parser", "pdf": {"engine": "pdf-text"}}] - return await _forward(openai_body, api_key, f"{_OPENROUTER_UPSTREAM}/chat/completions") + return await p_forward(openai_body, api_key, f"{P_OPENROUTER_UPSTREAM}/chat/completions") -def _estimate_body_bytes(body_json: dict) -> int: +def p_estimate_body_bytes(body_json: dict) -> int: """Sum the base64 payload bytes across content blocks. Used as a cheap pre-flight check before httpx serializes the body.""" total = 0 @@ -336,12 +336,12 @@ def _estimate_body_bytes(body_json: dict) -> int: return total -async def _forward( +async def p_forward( body_json: dict, api_key: str, url: str, ) -> tuple[int, AsyncIterator[bytes], dict[str, str]]: # Pre-flight size check. base64 expands ~4/3 so 40MB raw → 53MB b64. - raw_estimate = int(_estimate_body_bytes(body_json) * 0.75) - if raw_estimate > _BYPASS_MAX_RAW_BYTES: + raw_estimate = int(p_estimate_body_bytes(body_json) * 0.75) + if raw_estimate > P_BYPASS_MAX_RAW_BYTES: async def reject(): payload = json.dumps({ @@ -350,7 +350,7 @@ async def _forward( "type": "invalid_request_error", "message": ( f"Attached files total ~{raw_estimate // (1024*1024)} MB, " - f"over the {_BYPASS_MAX_RAW_BYTES // (1024*1024)} MB per-request " + f"over the {P_BYPASS_MAX_RAW_BYTES // (1024*1024)} MB per-request " "cap on this provider lane. Detach a file or split across " "separate turns." ), @@ -371,14 +371,14 @@ async def _forward( # ~40MB request body + a streaming response buffer, and the OS # OOM-kills the backend (observed on macOS during a 3-PDF probe # burst). Semaphore serializes excess requests instead of failing. - await _bypass_sema.acquire() + await p_bypass_sema.acquire() client = httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=30.0)) try: req = client.build_request("POST", url, json=body_json, headers=headers) upstream = await client.send(req, stream=True) except Exception: await client.aclose() - _bypass_sema.release() + p_bypass_sema.release() raise async def streamer(): @@ -387,7 +387,7 @@ async def _forward( raw = await upstream.aread() yield raw return - async for chunk in _translate_response_stream(upstream, body_json["model"]): + async for chunk in p_translate_response_stream(upstream, body_json["model"]): yield chunk finally: try: @@ -396,7 +396,7 @@ async def _forward( try: await client.aclose() finally: - _bypass_sema.release() + p_bypass_sema.release() return upstream.status_code, streamer(), { "content-type": "text/event-stream" if upstream.status_code < 400 else "application/json", diff --git a/backend/apps/agents/tools/ssrf_guard.py b/backend/apps/agents/tools/ssrf_guard.py index c641844a..08b98b3e 100644 --- a/backend/apps/agents/tools/ssrf_guard.py +++ b/backend/apps/agents/tools/ssrf_guard.py @@ -27,7 +27,7 @@ class SSRFBlocked(Exception): """A fetch was refused because it targets a forbidden IP range.""" -_BLOCKED_V4_NETS = [ +P_BLOCKED_V4_NETS = [ ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("172.16.0.0/12"), ipaddress.ip_network("192.168.0.0/16"), @@ -38,7 +38,7 @@ _BLOCKED_V4_NETS = [ ipaddress.ip_network("198.18.0.0/15"), # benchmarking ] -_BLOCKED_V6_NETS = [ +P_BLOCKED_V6_NETS = [ ipaddress.ip_network("fe80::/10"), # link-local ipaddress.ip_network("fc00::/7"), # ULA ipaddress.ip_network("ff00::/8"), # multicast @@ -46,7 +46,7 @@ _BLOCKED_V6_NETS = [ ] -async def _resolve_host_async(host: str) -> list[str]: +async def p_resolve_host_async(host: str) -> list[str]: """Resolve host to all IPs (v4 + v6) without blocking the event loop.""" loop = asyncio.get_event_loop() try: @@ -56,7 +56,7 @@ async def _resolve_host_async(host: str) -> list[str]: return list({info[4][0] for info in infos}) -def _is_forbidden_ip(ip_str: str) -> bool: +def p_is_forbidden_ip(ip_str: str) -> bool: """True iff this IP is in a blocked range. Loopback is allowed (see module docstring).""" try: ip = ipaddress.ip_address(ip_str) @@ -65,8 +65,8 @@ def _is_forbidden_ip(ip_str: str) -> bool: if ip.is_loopback: return False if ip.version == 4: - return any(ip in net for net in _BLOCKED_V4_NETS) - return any(ip in net for net in _BLOCKED_V6_NETS) + return any(ip in net for net in P_BLOCKED_V4_NETS) + return any(ip in net for net in P_BLOCKED_V6_NETS) async def assert_safe_url(url: str) -> str: @@ -88,17 +88,17 @@ async def assert_safe_url(url: str) -> str: try: ipaddress.ip_address(host) - if _is_forbidden_ip(host): + if p_is_forbidden_ip(host): raise SSRFBlocked(f"URL host {host} is in a blocked range.") return url except ValueError: pass - resolved = await _resolve_host_async(host) + resolved = await p_resolve_host_async(host) if not resolved: raise SSRFBlocked(f"No DNS records for {host}.") for ip in resolved: - if _is_forbidden_ip(ip): + if p_is_forbidden_ip(ip): raise SSRFBlocked(f"Host {host} resolves to forbidden IP {ip}.") return url diff --git a/backend/apps/agents/tools/web.py b/backend/apps/agents/tools/web.py index 657da32c..63052ae9 100644 --- a/backend/apps/agents/tools/web.py +++ b/backend/apps/agents/tools/web.py @@ -10,9 +10,9 @@ import httpx from backend.apps.agents.tools.base import BaseTool from backend.apps.agents.tools.ssrf_guard import SSRFBlocked, safe_fetch -_HTTP_TIMEOUT = 30 -_MAX_OUTPUT_BYTES = 250 * 1024 # ~250 KB covers ~95% of articles/wikis/docs. -_USER_AGENT = ( +P_HTTP_TIMEOUT = 30 +P_MAX_OUTPUT_BYTES = 250 * 1024 # ~250 KB covers ~95% of articles/wikis/docs. +P_USER_AGENT = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ) @@ -47,13 +47,13 @@ def anthropic_web_search_is_reliable(*, uses_direct_anthropic_api: bool, return bool(uses_direct_anthropic_api or is_pro) -def _truncate(text: str, limit: int = _MAX_OUTPUT_BYTES) -> str: +def p_truncate(text: str, limit: int = P_MAX_OUTPUT_BYTES) -> str: if len(text) > limit: return text[:limit] + "\n... (output truncated)" return text -def _strip_html(raw_html: str) -> str: +def p_strip_html(raw_html: str) -> str: """Naive but effective HTML to plain-text conversion.""" text = re.sub(r"<(script|style)[^>]*>.*?", "", raw_html, flags=re.DOTALL | re.IGNORECASE) text = re.sub(r"<[^>]+>", " ", text) @@ -64,13 +64,13 @@ def _strip_html(raw_html: str) -> str: class WebSearchTool(BaseTool): - name = "WebSearch" - description = ( + p_name = "WebSearch" + p_description = ( "Search the web using DuckDuckGo and return titles, URLs, and " "snippets for the top results." ) - def get_schema(self) -> dict: + def p_get_schema(self) -> dict: return { "type": "object", "properties": { @@ -88,12 +88,12 @@ class WebSearchTool(BaseTool): "additionalProperties": False, } - async def execute(self, input_data: dict) -> list[dict]: + async def p_execute(self, input_data: dict) -> list[dict]: query: str = input_data["query"] num_results: int = input_data.get("num_results", 5) try: - results = await self._search_ddg(query, num_results) + results = await self.search_ddg(query, num_results) if not results: return [{"type": "text", "text": f"No search results found for: {query}"}] return [{"type": "text", "text": results}] @@ -106,12 +106,12 @@ class WebSearchTool(BaseTool): return [{"type": "text", "text": f"Web search error: {exc}"}] @staticmethod - async def _search_ddg(query: str, num_results: int) -> str: + async def search_ddg(query: str, num_results: int) -> str: """Query DuckDuckGo HTML endpoint and parse results.""" async with httpx.AsyncClient( - timeout=_HTTP_TIMEOUT, + timeout=P_HTTP_TIMEOUT, follow_redirects=True, - headers={"User-Agent": _USER_AGENT}, + headers={"User-Agent": P_USER_AGENT}, ) as client: resp = await client.post( "https://html.duckduckgo.com/html/", @@ -160,14 +160,14 @@ class WebSearchTool(BaseTool): if "/y.js?" in raw_url or "ad_provider=" in raw_url or "ad_domain=" in raw_url: continue - title = _strip_html(link_match.group(2)).strip() + title = p_strip_html(link_match.group(2)).strip() snippet_match = re.search( r']*class="[^"]*result__snippet[^"]*"[^>]*>(.*?)', block, flags=re.DOTALL, ) - snippet = _strip_html(snippet_match.group(1)).strip() if snippet_match else "" + snippet = p_strip_html(snippet_match.group(1)).strip() if snippet_match else "" # DDG wraps URLs in a redirect; extract the real one. real_url_match = re.search(r"uddg=([^&]+)", raw_url) @@ -186,13 +186,13 @@ class WebSearchTool(BaseTool): class WebFetchTool(BaseTool): - name = "WebFetch" - description = ( + p_name = "WebFetch" + p_description = ( "Fetch the contents of a URL and return the extracted text. " "HTML is stripped to plain text. Output capped at ~250 KB." ) - def get_schema(self) -> dict: + def p_get_schema(self) -> dict: return { "type": "object", "properties": { @@ -217,8 +217,8 @@ class WebFetchTool(BaseTool): resp = await safe_fetch( url, method="GET", - headers={"User-Agent": _USER_AGENT}, - timeout=_HTTP_TIMEOUT, + headers={"User-Agent": P_USER_AGENT}, + timeout=P_HTTP_TIMEOUT, ) resp.raise_for_status() except SSRFBlocked as exc: @@ -245,11 +245,11 @@ class WebFetchTool(BaseTool): except Exception: text = None if not text: - text = _strip_html(resp.text) + text = p_strip_html(resp.text) else: text = resp.text - text = _truncate(text) + text = p_truncate(text) header = f"Contents of {url}:" if prompt: diff --git a/backend/apps/agents/web_mcp_server.py b/backend/apps/agents/web_mcp_server.py index 93fecb8b..09b827d9 100755 --- a/backend/apps/agents/web_mcp_server.py +++ b/backend/apps/agents/web_mcp_server.py @@ -76,7 +76,7 @@ def send_response(id_, result=None, error=None): sys.stdout.flush() -def _post(url: str, body: dict, timeout: float = 60.0) -> dict: +def p_post(url: str, body: dict, timeout: float = 60.0) -> dict: payload = json.dumps(body).encode() headers = {"Content-Type": "application/json"} if BACKEND_AUTH: @@ -107,7 +107,7 @@ def handle_tool_call(tool_name: str, arguments: dict) -> dict: body = {"query": query, "num_results": num} if PRIMARY_HINT: body["primary"] = PRIMARY_HINT - r = _post(SEARCH_URL, body, timeout=45.0) + r = p_post(SEARCH_URL, body, timeout=45.0) if "error" in r: return {"content": [{"type": "text", "text": f"Search failed: {r['error']}"}], "isError": True} results = r.get("results", "") @@ -127,7 +127,7 @@ def handle_tool_call(tool_name: str, arguments: dict) -> dict: body["prompt"] = str(prompt) if PRIMARY_HINT: body["primary"] = PRIMARY_HINT - r = _post(FETCH_URL, body, timeout=45.0) + r = p_post(FETCH_URL, body, timeout=45.0) if "error" in r: return {"content": [{"type": "text", "text": f"Fetch failed: {r['error']}"}], "isError": True} content = r.get("content", "") diff --git a/backend/apps/auth/router.py b/backend/apps/auth/router.py index 6765607c..14947865 100644 --- a/backend/apps/auth/router.py +++ b/backend/apps/auth/router.py @@ -40,14 +40,14 @@ async def auth_lifespan(): auth = SubApp("auth", auth_lifespan) -def _proxy_url() -> str: +def p_proxy_url() -> str: settings_obj = load_settings() url = (getattr(settings_obj, "openswarm_proxy_url", None) or OPENSWARM_DEFAULT_PROXY_URL) return url.rstrip("/") -async def _sync_pro_routing(settings_obj) -> None: +async def p_sync_pro_routing(settings_obj) -> None: """Mirror connection state into 9Router's Claude lane; sign-in can flip a paying user into pro mode and sign-out must tear the lane down so a revoked bearer doesn't linger in the router.""" @@ -58,11 +58,11 @@ async def _sync_pro_routing(settings_obj) -> None: logger.debug("pro routing sync skipped: %s", e) -def _sync_identity_to_service(settings_obj) -> None: +def p_sync_identity_to_service(settings_obj) -> None: """Push user_id + email + signin_method into the service-sync identify pipeline so every event from this user has the right Person properties.""" try: - from backend.apps.service.client import identify as _identify + from backend.apps.service.client import identify except Exception: return props = { @@ -73,7 +73,7 @@ def _sync_identity_to_service(settings_obj) -> None: if email: props["email"] = email try: - _identify(props) + identify(props) except Exception as e: logger.debug("identify sync failed: %s", e) @@ -101,7 +101,7 @@ async def signin_activate(body: SigninActivateRequest): if not body.token or len(body.token) < 16: raise HTTPException(status_code=400, detail="Invalid token") - proxy = _proxy_url() + proxy = p_proxy_url() try: async with httpx.AsyncClient(timeout=10.0) as client: r = await client.post( @@ -157,8 +157,8 @@ async def signin_activate(body: SigninActivateRequest): settings_obj.openswarm_proxy_url = proxy await save_settings_async(settings_obj) - _sync_identity_to_service(settings_obj) - await _sync_pro_routing(settings_obj) + p_sync_identity_to_service(settings_obj) + await p_sync_pro_routing(settings_obj) return { "ok": True, @@ -185,7 +185,7 @@ async def signout(): """ settings_obj = load_settings() bearer = getattr(settings_obj, "openswarm_bearer_token", None) - proxy = _proxy_url() + proxy = p_proxy_url() if bearer: try: async with httpx.AsyncClient(timeout=5.0) as client: @@ -245,6 +245,6 @@ async def signout(): settings_obj.openswarm_subscription_expires = None settings_obj.openswarm_usage_cached = None await save_settings_async(settings_obj) - _sync_identity_to_service(settings_obj) - await _sync_pro_routing(settings_obj) + p_sync_identity_to_service(settings_obj) + await p_sync_pro_routing(settings_obj) return {"ok": True} diff --git a/backend/apps/dashboard_layout/__init__.py b/backend/apps/dashboard_layout/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/apps/dashboard_layout/dashboard_layout.py b/backend/apps/dashboard_layout/dashboard_layout.py deleted file mode 100644 index 5d16b81b..00000000 --- a/backend/apps/dashboard_layout/dashboard_layout.py +++ /dev/null @@ -1,58 +0,0 @@ -import json -import os -import logging -from contextlib import asynccontextmanager -from backend.config.Apps import SubApp -from backend.apps.dashboard_layout.models import DashboardLayout, DashboardLayoutUpdate - -logger = logging.getLogger(__name__) - -from backend.config.paths import DASHBOARD_LAYOUT_DIR as DATA_DIR - -LAYOUT_FILE = os.path.join(DATA_DIR, "layout.json") - - -@asynccontextmanager -async def dashboard_layout_lifespan(): - os.makedirs(DATA_DIR, exist_ok=True) - yield - - -dashboard_layout = SubApp("dashboard_layout", dashboard_layout_lifespan) - - -def _default_layout() -> DashboardLayout: - return DashboardLayout(cards={}) - - -def _load() -> DashboardLayout: - if not os.path.exists(LAYOUT_FILE): - return _default_layout() - try: - with open(LAYOUT_FILE) as f: - data = json.load(f) - if "columns" in data and "cards" not in data: - logger.info("Detected old column-based layout format, resetting to empty canvas") - return _default_layout() - return DashboardLayout(**data) - except Exception: - logger.exception("Failed to load dashboard layout, returning default") - return _default_layout() - - -def _save(layout: DashboardLayout): - with open(LAYOUT_FILE, "w") as f: - json.dump(layout.model_dump(), f, indent=2) - - -@dashboard_layout.router.get("") -async def get_layout(): - layout = _load() - return layout.model_dump() - - -@dashboard_layout.router.put("") -async def update_layout(body: DashboardLayoutUpdate): - layout = DashboardLayout(cards=body.cards, view_cards=body.view_cards) - _save(layout) - return layout.model_dump() diff --git a/backend/apps/dashboard_layout/models.py b/backend/apps/dashboard_layout/models.py deleted file mode 100644 index 65193747..00000000 --- a/backend/apps/dashboard_layout/models.py +++ /dev/null @@ -1,27 +0,0 @@ -from pydantic import BaseModel, Field - - -class CardPosition(BaseModel): - session_id: str - x: float = 0 - y: float = 0 - width: float = 420 - height: float = 280 - - -class ViewCardPosition(BaseModel): - output_id: str - x: float = 0 - y: float = 0 - width: float = 480 - height: float = 360 - - -class DashboardLayout(BaseModel): - cards: dict[str, CardPosition] = Field(default_factory=dict) - view_cards: dict[str, ViewCardPosition] = Field(default_factory=dict) - - -class DashboardLayoutUpdate(BaseModel): - cards: dict[str, CardPosition] - view_cards: dict[str, ViewCardPosition] = Field(default_factory=dict) diff --git a/backend/apps/web/web.py b/backend/apps/web/web.py index 4f4b99bb..48731558 100644 --- a/backend/apps/web/web.py +++ b/backend/apps/web/web.py @@ -450,7 +450,7 @@ async def search(body: SearchBody) -> dict: # through to the slower-but-grounded backends. from backend.apps.agents.tools.web import WebSearchTool, DDGRateLimited try: - text = await WebSearchTool._search_ddg(body.query, body.num_results) + text = await WebSearchTool.search_ddg(body.query, body.num_results) except DDGRateLimited: # Surface the throttle as a recorded error (not a silent None) so the # caller can see WHY we fell through to a slower backend.