mirror of
https://github.com/usestrix/strix.git
synced 2026-08-24 03:42:37 +02:00
chore: nuke post-migration dead code, deps, and broken Dockerfile fallback
- Drop ``wait_for_http_ready`` (FastAPI sidecar healthcheck) — only Caido TCP probe survives now. Removes the ``httpx`` import. - Delete ``ListSitemapRenderer`` / ``ViewSitemapEntryRenderer`` — render UI for tools that disappeared with the Caido SDK migration. - Drop ``scrubadub`` runtime dep — PII sanitizer was nuked previously but the dep stayed; resolve strips 18 transitives (numpy, scipy, scikit-learn, nltk, faker, …). - Drop empty ``[project.optional-dependencies] sandbox`` section — last in-container Python dep migrated out. - Drop unused mypy overrides (``pydantic_settings``, ``jwt``, ``gql``, ``scrubadub``, ``httpx``) and the stale ``fastapi`` isort group. - Collapse Dockerfile's ``pipx install -r ... 2>/dev/null || venv`` fallback into a direct venv install — pipx never accepted ``-r`` so the fallback was always firing.
This commit is contained in:
@@ -459,152 +459,3 @@ class ScopeRulesRenderer(BaseToolRenderer):
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(text, classes=css_classes)
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
class ListSitemapRenderer(BaseToolRenderer):
|
||||
tool_name: ClassVar[str] = "list_sitemap"
|
||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||
|
||||
@classmethod
|
||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915
|
||||
args = tool_data.get("args", {})
|
||||
result = tool_data.get("result")
|
||||
status = tool_data.get("status", "running")
|
||||
|
||||
parent_id = args.get("parent_id")
|
||||
scope_id = args.get("scope_id")
|
||||
depth = args.get("depth")
|
||||
|
||||
text = Text()
|
||||
text.append(PROXY_ICON, style="dim")
|
||||
text.append(" listing sitemap", style="#06b6d4")
|
||||
|
||||
if parent_id:
|
||||
text.append(f" under #{_truncate(str(parent_id), 20)}", style="dim")
|
||||
|
||||
meta_parts = []
|
||||
if scope_id and isinstance(scope_id, str):
|
||||
meta_parts.append(f"scope:{scope_id[:8]}")
|
||||
if depth and depth != "DIRECT":
|
||||
meta_parts.append(depth.lower())
|
||||
if meta_parts:
|
||||
text.append(f" ({', '.join(meta_parts)})", style="dim")
|
||||
|
||||
if status == "completed" and isinstance(result, dict):
|
||||
if "error" in result:
|
||||
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||
else:
|
||||
total = result.get("total_count", 0)
|
||||
entries = result.get("entries", [])
|
||||
|
||||
text.append(f" [{total} entries]", style="dim")
|
||||
|
||||
if entries and isinstance(entries, list):
|
||||
text.append("\n")
|
||||
for i, entry in enumerate(entries[:MAX_REQUESTS_DISPLAY]):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
kind = entry.get("kind") or "?"
|
||||
label = entry.get("label") or "?"
|
||||
has_children = entry.get("hasDescendants", False)
|
||||
req = entry.get("request") or {}
|
||||
|
||||
kind_style = {
|
||||
"DOMAIN": "#f59e0b",
|
||||
"DIRECTORY": "#3b82f6",
|
||||
"REQUEST": "#22c55e",
|
||||
}.get(kind, "dim")
|
||||
|
||||
text.append(" ")
|
||||
kind_abbr = kind[:3] if isinstance(kind, str) else "?"
|
||||
text.append(f"{kind_abbr:3}", style=kind_style)
|
||||
text.append(f" {_truncate(label, 150)}", style="dim")
|
||||
|
||||
if req:
|
||||
method = req.get("method", "")
|
||||
code = req.get("status")
|
||||
if method:
|
||||
text.append(f" {method}", style="#a78bfa")
|
||||
if code:
|
||||
text.append(f" {code}", style=_status_style(code))
|
||||
|
||||
if has_children:
|
||||
text.append(" +", style="dim italic")
|
||||
|
||||
if i < min(len(entries), MAX_REQUESTS_DISPLAY) - 1:
|
||||
text.append("\n")
|
||||
|
||||
if len(entries) > MAX_REQUESTS_DISPLAY:
|
||||
text.append("\n")
|
||||
text.append(
|
||||
f" ... +{len(entries) - MAX_REQUESTS_DISPLAY} more", style="dim italic"
|
||||
)
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(text, classes=css_classes)
|
||||
|
||||
|
||||
@register_tool_renderer
|
||||
class ViewSitemapEntryRenderer(BaseToolRenderer):
|
||||
tool_name: ClassVar[str] = "view_sitemap_entry"
|
||||
css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"]
|
||||
|
||||
@classmethod
|
||||
def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912
|
||||
args = tool_data.get("args", {})
|
||||
result = tool_data.get("result")
|
||||
status = tool_data.get("status", "running")
|
||||
|
||||
entry_id = args.get("entry_id", "")
|
||||
|
||||
text = Text()
|
||||
text.append(PROXY_ICON, style="dim")
|
||||
text.append(" viewing sitemap", style="#06b6d4")
|
||||
|
||||
if entry_id:
|
||||
text.append(f" #{_truncate(str(entry_id), 20)}", style="dim")
|
||||
|
||||
if status == "completed" and isinstance(result, dict):
|
||||
if "error" in result:
|
||||
text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444")
|
||||
elif "entry" in result:
|
||||
entry = result.get("entry") or {}
|
||||
if not isinstance(entry, dict):
|
||||
entry = {}
|
||||
kind = entry.get("kind", "")
|
||||
label = entry.get("label", "")
|
||||
related = entry.get("related_requests") or {}
|
||||
related_reqs = related.get("requests", []) if isinstance(related, dict) else []
|
||||
total_related = related.get("total_count", 0) if isinstance(related, dict) else 0
|
||||
|
||||
if kind and label:
|
||||
text.append(f" {kind}: {_truncate(label, 120)}", style="dim")
|
||||
|
||||
if total_related:
|
||||
text.append(f" [{total_related} requests]", style="dim")
|
||||
|
||||
if related_reqs and isinstance(related_reqs, list):
|
||||
text.append("\n")
|
||||
for i, req in enumerate(related_reqs[:10]):
|
||||
if not isinstance(req, dict):
|
||||
continue
|
||||
method = req.get("method", "?")
|
||||
path = req.get("path", "/")
|
||||
code = req.get("status")
|
||||
|
||||
text.append(" ")
|
||||
text.append(f"{method:6}", style="#a78bfa")
|
||||
text.append(f" {_truncate(path, 180)}", style="dim")
|
||||
if code:
|
||||
text.append(f" {code}", style=_status_style(code))
|
||||
|
||||
if i < min(len(related_reqs), 10) - 1:
|
||||
text.append("\n")
|
||||
|
||||
if len(related_reqs) > 10:
|
||||
text.append("\n")
|
||||
text.append(f" ... +{len(related_reqs) - 10} more", style="dim italic")
|
||||
|
||||
css_classes = cls.get_css_classes(status)
|
||||
return Static(text, classes=css_classes)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Strix sandbox layer on top of OpenAI Agents SDK SandboxAgent / Manifest.
|
||||
|
||||
- :mod:`.healthcheck` — ``wait_for_http_ready`` / ``wait_for_tcp_ready``.
|
||||
- :mod:`.healthcheck` — ``wait_for_tcp_ready`` for Caido bring-up.
|
||||
- :mod:`.session_manager` — ``create_or_reuse`` / ``cleanup`` keyed
|
||||
by scan id.
|
||||
"""
|
||||
|
||||
@@ -1,81 +1,31 @@
|
||||
"""Sandbox port readiness probes used during session bring-up.
|
||||
"""Sandbox port readiness probe used during session bring-up.
|
||||
|
||||
The in-container tool server (FastAPI) takes a few seconds to start
|
||||
listening after the Docker container is created, and Caido's HTTPS
|
||||
proxy takes a similar window. The session manager waits for both
|
||||
before returning a session bundle so that the first tool call from
|
||||
an agent doesn't hit a connection refused.
|
||||
Caido's HTTPS proxy takes a few seconds to start listening after the
|
||||
Docker container is created. The session manager waits for it before
|
||||
returning a session bundle so that the first tool call from an agent
|
||||
doesn't hit a connection refused.
|
||||
|
||||
Two helpers are exposed:
|
||||
|
||||
- :func:`wait_for_http_ready` for the FastAPI tool server, whose
|
||||
``/health`` endpoint returns ``{"status": "healthy"}`` once the
|
||||
process is up. We don't require the JSON shape exactly — any 2xx
|
||||
is treated as ready.
|
||||
|
||||
- :func:`wait_for_tcp_ready` for Caido, which serves an HTTP forward
|
||||
proxy on its port and does *not* expose ``/health``. A TCP connect
|
||||
is the most we can probe without sending real proxy traffic.
|
||||
:func:`wait_for_tcp_ready` is the only probe — Caido serves an HTTP
|
||||
forward proxy on its port and does *not* expose ``/health``. A TCP
|
||||
connect is the most we can probe without sending real proxy traffic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SandboxNotReadyError(Exception):
|
||||
"""Raised when a sandbox port doesn't accept connections in time."""
|
||||
|
||||
|
||||
# Default per-attempt HTTP timeout. 5s so a slow first request (image
|
||||
# still warming up) doesn't misfire as a hard failure on a single attempt.
|
||||
_DEFAULT_HTTP_PROBE_TIMEOUT = 5.0
|
||||
|
||||
# Default polling cadence between attempts. Balanced for CI-style
|
||||
# fast bring-up (sub-second) without burning CPU when the port is
|
||||
# legitimately taking a few seconds.
|
||||
_DEFAULT_POLL_INTERVAL = 0.5
|
||||
|
||||
|
||||
async def wait_for_http_ready(
|
||||
url: str,
|
||||
*,
|
||||
timeout: float = 30.0,
|
||||
poll_interval: float = _DEFAULT_POLL_INTERVAL,
|
||||
probe_timeout: float = _DEFAULT_HTTP_PROBE_TIMEOUT,
|
||||
) -> None:
|
||||
"""Poll ``url`` until any 2xx response, or raise after ``timeout``.
|
||||
|
||||
Network errors (ConnectError / TimeoutException / RequestError)
|
||||
are treated as "not ready yet" — the loop continues. Any other
|
||||
exception class will surface immediately so a programmer error
|
||||
(bad URL, etc.) doesn't get silently retried for 30 seconds.
|
||||
"""
|
||||
deadline = asyncio.get_event_loop().time() + timeout
|
||||
last_error: str | None = None
|
||||
async with httpx.AsyncClient(timeout=probe_timeout, trust_env=False) as client:
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
try:
|
||||
response = await client.get(url)
|
||||
if 200 <= response.status_code < 300:
|
||||
return
|
||||
last_error = f"HTTP {response.status_code}"
|
||||
except (httpx.ConnectError, httpx.TimeoutException, httpx.RequestError) as e:
|
||||
last_error = type(e).__name__
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
raise SandboxNotReadyError(
|
||||
f"HTTP probe of {url} did not return 2xx within {timeout}s (last error: {last_error})",
|
||||
)
|
||||
|
||||
|
||||
async def wait_for_tcp_ready(
|
||||
host: str,
|
||||
port: int,
|
||||
|
||||
Reference in New Issue
Block a user