mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f170619d45 | ||
|
|
78d92ddd83 |
@@ -0,0 +1,20 @@
|
||||
"""PyInstaller runtime hook: point SSL env vars at the bundled certifi CA file.
|
||||
|
||||
Without ``collect_data_files('certifi')`` and this hook, a frozen binary can
|
||||
resolve ``certifi.where()`` to a missing path and fail TLS verification with a
|
||||
generic ``Connection error`` from httpx/litellm.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
if getattr(sys, "frozen", False):
|
||||
import os
|
||||
|
||||
import certifi
|
||||
|
||||
ca_bundle = certifi.where()
|
||||
os.environ.setdefault("SSL_CERT_FILE", ca_bundle)
|
||||
os.environ.setdefault("REQUESTS_CA_BUNDLE", ca_bundle)
|
||||
@@ -232,6 +232,8 @@ ignore = [
|
||||
"tests/test_codex_auth.py" = ["S105", "S106", "SLF001"]
|
||||
# Hatchling loads the build hook by path, not as an importable package.
|
||||
"scripts/tui_sidecar_hook.py" = ["INP001"]
|
||||
# PyInstaller runtime hooks are loaded by path, not as an importable package.
|
||||
"hooks/rthooks/pyi_rth_certifi.py" = ["INP001"]
|
||||
# Stdlib HTTP handler overrides (do_GET/do_POST).
|
||||
"strix/interface/auth_cli.py" = ["N802"]
|
||||
"tests/test_codex_streaming.py" = ["N802"]
|
||||
|
||||
+4
-1
@@ -40,6 +40,9 @@ datas += collect_data_files('tiktoken')
|
||||
datas += collect_data_files('tiktoken_ext')
|
||||
|
||||
datas += collect_data_files('litellm')
|
||||
# Frozen binaries need certifi's CA bundle on disk; without it TLS to LLM
|
||||
# providers fails with a generic httpx/litellm "Connection error".
|
||||
datas += collect_data_files('certifi')
|
||||
|
||||
datas += collect_data_files('agents', includes=['**/*.md', '**/*.jinja', '**/*.json'])
|
||||
|
||||
@@ -251,7 +254,7 @@ a = Analysis(
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
runtime_hooks=[str(project_root / 'hooks' / 'rthooks' / 'pyi_rth_certifi.py')],
|
||||
excludes=excludes,
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
|
||||
+37
-6
@@ -6,6 +6,7 @@ Strix Agent Interface
|
||||
import argparse
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -42,7 +43,23 @@ from strix.interface.utils import (
|
||||
build_final_stats_text,
|
||||
)
|
||||
from strix.telemetry import posthog, scarf
|
||||
from strix.telemetry.logging import configure_dependency_logging
|
||||
from strix.telemetry.logging import (
|
||||
attach_preflight_logging,
|
||||
configure_dependency_logging,
|
||||
debug_logging_enabled,
|
||||
)
|
||||
|
||||
|
||||
# Frozen (PyInstaller) binaries need the bundled certifi CA path exported so
|
||||
# httpx/requests verify TLS against a real cacert.pem inside the archive.
|
||||
# The PyInstaller runtime hook covers the official build; this is an extra
|
||||
# safety net for any frozen entry that loads this module.
|
||||
if getattr(sys, "frozen", False):
|
||||
import certifi
|
||||
|
||||
_ca_bundle = certifi.where()
|
||||
os.environ.setdefault("SSL_CERT_FILE", _ca_bundle)
|
||||
os.environ.setdefault("REQUESTS_CA_BUNDLE", _ca_bundle)
|
||||
|
||||
|
||||
BEDROCK_MODEL_PREFIX = "bedrock/"
|
||||
@@ -57,9 +74,6 @@ VERTEX_EXTRA_HINT = (
|
||||
)
|
||||
|
||||
|
||||
import logging # noqa: E402
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -352,16 +366,30 @@ def _print_error_panel(title: str, message: str) -> None:
|
||||
console.print()
|
||||
|
||||
|
||||
def _format_connection_error_detail(exc: BaseException) -> str:
|
||||
"""Return the user-facing error detail for a model connection failure.
|
||||
|
||||
With ``STRIX_DEBUG`` enabled, include the full ``__cause__`` /
|
||||
``__context__`` chain so wrapped TLS failures (e.g.
|
||||
``SSLCertVerificationError`` under litellm/httpx ``Connection error``)
|
||||
are visible.
|
||||
"""
|
||||
if debug_logging_enabled():
|
||||
return " | ".join(_exception_messages(exc))
|
||||
return str(exc)
|
||||
|
||||
|
||||
def _print_model_connection_error(exc: BaseException, model_name: str) -> None:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
detail = _format_connection_error_detail(exc)
|
||||
sub_hint = _subscription_error_hint(exc)
|
||||
if sub_hint is not None:
|
||||
border_style = "yellow"
|
||||
error_text.append("MODEL NOT AVAILABLE ON SUBSCRIPTION", style="bold yellow")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append(f"{sub_hint}\n", style="white")
|
||||
error_text.append(f"\nDetails: {exc}", style="dim white")
|
||||
error_text.append(f"\nDetails: {detail}", style="dim white")
|
||||
else:
|
||||
border_style = "red"
|
||||
error_text.append("LLM CONNECTION FAILED", style="bold red")
|
||||
@@ -371,7 +399,7 @@ def _print_model_connection_error(exc: BaseException, model_name: str) -> None:
|
||||
hint = _provider_import_hint(exc, model_name)
|
||||
if hint is not None:
|
||||
error_text.append(f"\n{hint}\n", style="bold yellow")
|
||||
error_text.append(f"\nError: {exc}", style="dim white")
|
||||
error_text.append(f"\nError: {detail}", style="dim white")
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
@@ -395,6 +423,9 @@ def _bootstrap_scan(args: argparse.Namespace) -> None:
|
||||
validate_environment()
|
||||
if not args.non_interactive:
|
||||
return
|
||||
# Preflight runs before prepare_run()/setup_scan_logging(), so attach a
|
||||
# stderr handler now or STRIX_DEBUG=1 never shows warm-up failures.
|
||||
attach_preflight_logging()
|
||||
try:
|
||||
asyncio.run(warm_up_llm(show_model_warning=True))
|
||||
except ModelConnectionError as exc:
|
||||
|
||||
@@ -116,6 +116,69 @@ def _silence_urllib3_finalizer_noise() -> None:
|
||||
sys.unraisablehook = hook
|
||||
|
||||
|
||||
_DEBUG_ENV_TRUTHY = frozenset({"1", "true", "yes", "on"})
|
||||
_PREFLIGHT_HANDLER_TAG = "_strix_preflight_handler"
|
||||
|
||||
|
||||
def debug_logging_enabled(*, debug: bool | None = None) -> bool:
|
||||
"""Resolve whether Strix debug logging is on.
|
||||
|
||||
``None`` (default) reads ``STRIX_DEBUG``: ``1`` / ``true`` / ``yes`` /
|
||||
``on`` (case-insensitive) enables debug.
|
||||
"""
|
||||
if debug is not None:
|
||||
return debug
|
||||
return (os.environ.get("STRIX_DEBUG") or "").strip().lower() in _DEBUG_ENV_TRUTHY
|
||||
|
||||
|
||||
def attach_preflight_logging(*, debug: bool | None = None) -> None:
|
||||
"""Attach a stderr-only handler so LLM preflight logs are visible early.
|
||||
|
||||
``warm_up_llm`` runs before ``setup_scan_logging`` (which needs a run
|
||||
directory). Without this, ``STRIX_DEBUG=1`` still produces no output for
|
||||
preflight failures.
|
||||
"""
|
||||
configure_dependency_logging()
|
||||
enabled = debug_logging_enabled(debug=debug)
|
||||
level = logging.DEBUG if enabled else logging.ERROR
|
||||
|
||||
formatter = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
|
||||
context_filter = _StrixContextFilter()
|
||||
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setLevel(level)
|
||||
stream_handler.setFormatter(formatter)
|
||||
stream_handler.addFilter(context_filter)
|
||||
stream_handler.addFilter(_StdoutQuietFilter())
|
||||
setattr(stream_handler, _PREFLIGHT_HANDLER_TAG, True)
|
||||
|
||||
for name in _TRACKED_ROOTS:
|
||||
tracked = logging.getLogger(name)
|
||||
# Replace a previous preflight handler so repeated calls stay idempotent.
|
||||
for handler in list(tracked.handlers):
|
||||
if getattr(handler, _PREFLIGHT_HANDLER_TAG, False):
|
||||
tracked.removeHandler(handler)
|
||||
with contextlib.suppress(Exception):
|
||||
handler.close()
|
||||
tracked.setLevel(logging.DEBUG)
|
||||
tracked.addHandler(stream_handler)
|
||||
tracked.propagate = False
|
||||
|
||||
for name in _NOISY_LIBS:
|
||||
logging.getLogger(name).setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def remove_preflight_logging() -> None:
|
||||
"""Detach any preflight stderr handler from the tracked logger roots."""
|
||||
for name in _TRACKED_ROOTS:
|
||||
tracked = logging.getLogger(name)
|
||||
for handler in list(tracked.handlers):
|
||||
if getattr(handler, _PREFLIGHT_HANDLER_TAG, False):
|
||||
tracked.removeHandler(handler)
|
||||
with contextlib.suppress(Exception):
|
||||
handler.close()
|
||||
|
||||
|
||||
def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[[], None]:
|
||||
"""Attach scan-scoped handlers; return a teardown callable.
|
||||
|
||||
@@ -133,14 +196,9 @@ def setup_scan_logging(run_dir: Path, *, debug: bool | None = None) -> Callable[
|
||||
time. Safe to call from a ``finally`` block.
|
||||
"""
|
||||
configure_dependency_logging()
|
||||
remove_preflight_logging()
|
||||
|
||||
if debug is None:
|
||||
debug = (os.environ.get("STRIX_DEBUG") or "").strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
debug = debug_logging_enabled(debug=debug)
|
||||
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_path = run_dir / "strix.log"
|
||||
|
||||
@@ -9,6 +9,8 @@ import pytest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC_PATH = PROJECT_ROOT / "strix.spec"
|
||||
CERTIFI_RTHOOK = PROJECT_ROOT / "hooks" / "rthooks" / "pyi_rth_certifi.py"
|
||||
|
||||
|
||||
def test_wheel_build_requires_go(tmp_path: Path) -> None:
|
||||
@@ -29,3 +31,15 @@ def test_wheel_build_requires_go(tmp_path: Path) -> None:
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "Go 1.24 or newer is required" in result.stdout + result.stderr
|
||||
|
||||
|
||||
def test_pyinstaller_spec_bundles_certifi_ca_and_runtime_hook() -> None:
|
||||
spec = SPEC_PATH.read_text(encoding="utf-8")
|
||||
assert "collect_data_files('certifi')" in spec
|
||||
assert "pyi_rth_certifi.py" in spec
|
||||
assert "runtime_hooks=[" in spec
|
||||
assert CERTIFI_RTHOOK.is_file()
|
||||
hook = CERTIFI_RTHOOK.read_text(encoding="utf-8")
|
||||
assert "SSL_CERT_FILE" in hook
|
||||
assert "REQUESTS_CA_BUNDLE" in hook
|
||||
assert "certifi.where()" in hook
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Tests for exception-chain helpers and preflight debug logging."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import ssl
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from strix.interface.main import (
|
||||
_exception_messages,
|
||||
_format_connection_error_detail,
|
||||
)
|
||||
from strix.telemetry import logging as tlog
|
||||
from strix.telemetry.logging import (
|
||||
attach_preflight_logging,
|
||||
debug_logging_enabled,
|
||||
remove_preflight_logging,
|
||||
setup_scan_logging,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _preflight_handlers(name: str) -> list[logging.Handler]:
|
||||
return [
|
||||
handler
|
||||
for handler in logging.getLogger(name).handlers
|
||||
if getattr(handler, tlog._PREFLIGHT_HANDLER_TAG, False)
|
||||
]
|
||||
|
||||
|
||||
def test_exception_messages_walks_cause_chain_to_ssl_error() -> None:
|
||||
root = ssl.SSLCertVerificationError("certificate verify failed")
|
||||
middle = ConnectionError("TLS handshake failed")
|
||||
middle.__cause__ = root
|
||||
exc = ConnectionError("Connection error.")
|
||||
exc.__cause__ = middle
|
||||
|
||||
messages = _exception_messages(exc)
|
||||
|
||||
assert "Connection error." in messages
|
||||
assert "TLS handshake failed" in messages
|
||||
assert any("certificate verify failed" in message for message in messages)
|
||||
|
||||
|
||||
def test_format_connection_error_detail_includes_chain_when_debug(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
root = ssl.SSLCertVerificationError("certificate verify failed")
|
||||
exc = ConnectionError("Connection error.")
|
||||
exc.__cause__ = root
|
||||
|
||||
monkeypatch.delenv("STRIX_DEBUG", raising=False)
|
||||
assert _format_connection_error_detail(exc) == "Connection error."
|
||||
|
||||
monkeypatch.setenv("STRIX_DEBUG", "1")
|
||||
detail = _format_connection_error_detail(exc)
|
||||
assert "Connection error." in detail
|
||||
assert "certificate verify failed" in detail
|
||||
|
||||
|
||||
def test_debug_logging_enabled_reads_strix_debug(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("STRIX_DEBUG", raising=False)
|
||||
assert debug_logging_enabled() is False
|
||||
assert debug_logging_enabled(debug=True) is True
|
||||
|
||||
monkeypatch.setenv("STRIX_DEBUG", "yes")
|
||||
assert debug_logging_enabled() is True
|
||||
|
||||
|
||||
def test_attach_preflight_logging_emits_debug_to_stderr(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
monkeypatch.setenv("STRIX_DEBUG", "1")
|
||||
try:
|
||||
attach_preflight_logging()
|
||||
logging.getLogger("strix").debug("LLM warm-up failed")
|
||||
captured = capsys.readouterr()
|
||||
assert "LLM warm-up failed" in captured.err
|
||||
finally:
|
||||
remove_preflight_logging()
|
||||
|
||||
|
||||
def test_setup_scan_logging_removes_preflight_handler(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.delenv("STRIX_DEBUG", raising=False)
|
||||
attach_preflight_logging()
|
||||
assert _preflight_handlers("strix")
|
||||
|
||||
teardown = setup_scan_logging(tmp_path)
|
||||
try:
|
||||
for name in ("strix", "openai.agents"):
|
||||
assert not _preflight_handlers(name)
|
||||
finally:
|
||||
teardown()
|
||||
Reference in New Issue
Block a user