mirror of
https://github.com/usestrix/strix.git
synced 2026-10-01 02:03:55 +00:00
- check_docker_installed/connection -> check_runtime_installed/connection - pull_docker_image -> pull_runtime_image - get_docker_client -> get_runtime_client - track backend_name on sandbox session/client for accurate error context - drop unused HOST_GATEWAY_HOSTNAME constant
266 lines
10 KiB
Python
266 lines
10 KiB
Python
"""Startup environment validation and sandbox image management."""
|
|
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import sys
|
|
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.text import Text
|
|
|
|
from strix.config import IntegrationSettings, codex, load_settings
|
|
from strix.interface.utils import (
|
|
check_runtime_connection,
|
|
image_exists,
|
|
process_pull_line,
|
|
)
|
|
from strix.telemetry import report_error
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _missing_web_search_vars(integrations: IntegrationSettings) -> list[str]:
|
|
"""Mirror the web_search provider rules: which key(s) the selected provider needs."""
|
|
if integrations.web_search_provider == "exa":
|
|
return [] if integrations.exa_api_key else ["EXA_API_KEY"]
|
|
if integrations.web_search_provider == "perplexity":
|
|
return [] if integrations.perplexity_api_key else ["PERPLEXITY_API_KEY"]
|
|
if integrations.exa_api_key or integrations.perplexity_api_key:
|
|
return []
|
|
return ["EXA_API_KEY", "PERPLEXITY_API_KEY"]
|
|
|
|
|
|
def validate_environment() -> None:
|
|
logger.info("Validating environment")
|
|
console = Console()
|
|
missing_required_vars = []
|
|
missing_optional_vars = []
|
|
|
|
settings = load_settings()
|
|
|
|
if codex.subscription_model(settings.llm.model):
|
|
if not codex.is_authenticated():
|
|
console.print(
|
|
f"[red]STRIX_LLM={settings.llm.model} uses your ChatGPT subscription, "
|
|
"but you're not signed in.[/] Run [cyan]strix auth login chatgpt[/] first."
|
|
)
|
|
report_error("subscription_not_signed_in")
|
|
sys.exit(1)
|
|
logger.info("Environment OK (ChatGPT subscription)")
|
|
return
|
|
|
|
if not settings.llm.model:
|
|
missing_required_vars.append("STRIX_LLM")
|
|
|
|
if not settings.llm.api_key:
|
|
missing_optional_vars.append("LLM_API_KEY")
|
|
|
|
if not settings.llm.api_base:
|
|
missing_optional_vars.append("LLM_API_BASE")
|
|
|
|
missing_optional_vars.extend(_missing_web_search_vars(settings.integrations))
|
|
|
|
if missing_required_vars:
|
|
error_text = Text()
|
|
error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red")
|
|
error_text.append("\n\n", style="white")
|
|
|
|
for var in missing_required_vars:
|
|
error_text.append(f"• {var}", style="bold yellow")
|
|
error_text.append(" is not set\n", style="white")
|
|
|
|
if missing_optional_vars:
|
|
error_text.append("\nOptional environment variables:\n", style="dim white")
|
|
for var in missing_optional_vars:
|
|
error_text.append(f"• {var}", style="dim yellow")
|
|
error_text.append(" is not set\n", style="dim white")
|
|
|
|
error_text.append("\nRequired environment variables:\n", style="white")
|
|
for var in missing_required_vars:
|
|
if var == "STRIX_LLM":
|
|
error_text.append("• ", style="white")
|
|
error_text.append("STRIX_LLM", style="bold cyan")
|
|
error_text.append(
|
|
" - Model name to use (e.g., 'openrouter/z-ai/glm-5.3' or "
|
|
"'anthropic/claude-opus-4-7')\n",
|
|
style="white",
|
|
)
|
|
|
|
if missing_optional_vars:
|
|
error_text.append("\nOptional environment variables:\n", style="white")
|
|
for var in missing_optional_vars:
|
|
if var == "LLM_API_BASE":
|
|
error_text.append("• ", style="white")
|
|
error_text.append("LLM_API_BASE", style="bold cyan")
|
|
error_text.append(
|
|
" - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n",
|
|
style="white",
|
|
)
|
|
elif var == "PERPLEXITY_API_KEY":
|
|
error_text.append("• ", style="white")
|
|
error_text.append("PERPLEXITY_API_KEY", style="bold cyan")
|
|
error_text.append(
|
|
" - API key for Perplexity AI web search (alternative to Exa)\n",
|
|
style="white",
|
|
)
|
|
elif var == "EXA_API_KEY":
|
|
error_text.append("• ", style="white")
|
|
error_text.append("EXA_API_KEY", style="bold cyan")
|
|
error_text.append(
|
|
" - API key for Exa web search (enables real-time research)\n",
|
|
style="white",
|
|
)
|
|
elif var == "STRIX_REASONING_EFFORT":
|
|
error_text.append("• ", style="white")
|
|
error_text.append("STRIX_REASONING_EFFORT", style="bold cyan")
|
|
error_text.append(
|
|
" - Reasoning effort level: none, minimal, low, medium, high, xhigh, "
|
|
"max (default: high)\n",
|
|
style="white",
|
|
)
|
|
|
|
error_text.append("\nExample setup:\n", style="white")
|
|
error_text.append("export STRIX_LLM='openrouter/z-ai/glm-5.3'\n", style="dim white")
|
|
|
|
if missing_optional_vars:
|
|
for var in missing_optional_vars:
|
|
if var == "LLM_API_BASE":
|
|
error_text.append(
|
|
"export LLM_API_BASE='http://localhost:11434' "
|
|
"# needed for local models only\n",
|
|
style="dim white",
|
|
)
|
|
elif var == "PERPLEXITY_API_KEY":
|
|
error_text.append(
|
|
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
|
|
)
|
|
elif var == "EXA_API_KEY":
|
|
error_text.append("export EXA_API_KEY='your-exa-key-here'\n", style="dim white")
|
|
elif var == "STRIX_REASONING_EFFORT":
|
|
error_text.append(
|
|
"export STRIX_REASONING_EFFORT='high'\n",
|
|
style="dim white",
|
|
)
|
|
|
|
panel = Panel(
|
|
error_text,
|
|
title="[bold white]STRIX",
|
|
title_align="left",
|
|
border_style="red",
|
|
padding=(1, 2),
|
|
)
|
|
|
|
logger.debug("Missing required env vars: %s", missing_required_vars)
|
|
console.print("\n")
|
|
console.print(panel)
|
|
console.print()
|
|
report_error("missing_required_config")
|
|
sys.exit(1)
|
|
logger.info(
|
|
"Environment OK (optional missing: %s)",
|
|
missing_optional_vars or "none",
|
|
)
|
|
|
|
|
|
def check_runtime_installed() -> None:
|
|
backend = os.environ.get("STRIX_RUNTIME_BACKEND", "").strip()
|
|
if not backend:
|
|
try:
|
|
backend = getattr(load_settings().runtime, "backend", "docker")
|
|
except Exception:
|
|
backend = "docker"
|
|
backend = (backend or "docker").lower()
|
|
cli_name = "podman" if backend == "podman" else "docker"
|
|
display_name = "Podman" if backend == "podman" else "Docker"
|
|
|
|
if backend == "podman":
|
|
installed = shutil.which("podman") is not None or shutil.which("docker") is not None
|
|
else:
|
|
installed = shutil.which("docker") is not None
|
|
|
|
if not installed:
|
|
logger.debug("%s CLI not found in PATH", display_name)
|
|
console = Console()
|
|
error_text = Text()
|
|
error_text.append(f"{display_name.upper()} NOT INSTALLED", style="bold red")
|
|
error_text.append("\n\n", style="white")
|
|
error_text.append(f"The '{cli_name}' CLI was not found in your PATH.\n", style="white")
|
|
error_text.append(
|
|
f"Please install {display_name} and ensure the '{cli_name}' command is available.\n\n",
|
|
style="white",
|
|
)
|
|
|
|
panel = Panel(
|
|
error_text,
|
|
title="[bold white]STRIX",
|
|
title_align="left",
|
|
border_style="red",
|
|
padding=(1, 2),
|
|
)
|
|
console.print("\n", panel, "\n")
|
|
report_error(f"{backend}_not_installed")
|
|
sys.exit(1)
|
|
logger.debug("%s CLI present", display_name)
|
|
|
|
|
|
def pull_runtime_image() -> None:
|
|
from docker.errors import DockerException
|
|
|
|
console = Console()
|
|
backend = os.environ.get("STRIX_RUNTIME_BACKEND", "").strip()
|
|
if not backend:
|
|
try:
|
|
backend = getattr(load_settings().runtime, "backend", "docker")
|
|
except Exception:
|
|
backend = "docker"
|
|
backend = (backend or "docker").lower()
|
|
display_name = "Podman" if backend == "podman" else "Docker"
|
|
client = check_runtime_connection(backend)
|
|
|
|
image = load_settings().runtime.image
|
|
|
|
if image_exists(client, image):
|
|
logger.debug("%s image already present locally: %s", display_name, image)
|
|
return
|
|
|
|
logger.info("Pulling %s image: %s", display_name.lower(), image)
|
|
console.print()
|
|
console.print(f"[dim]Pulling image[/] {image}")
|
|
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
|
console.print()
|
|
|
|
with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status:
|
|
try:
|
|
layers_info: dict[str, str] = {}
|
|
last_update = ""
|
|
|
|
for line in client.api.pull(image, stream=True, decode=True):
|
|
last_update = process_pull_line(line, layers_info, status, last_update)
|
|
|
|
except DockerException as e:
|
|
logger.debug("Failed to pull %s image %s", display_name.lower(), image, exc_info=True)
|
|
console.print()
|
|
error_text = Text()
|
|
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
|
error_text.append("\n\n", style="white")
|
|
error_text.append(f"Could not download: {image}\n", style="white")
|
|
error_text.append(str(e), style="dim red")
|
|
|
|
panel = Panel(
|
|
error_text,
|
|
title="[bold white]STRIX",
|
|
title_align="left",
|
|
border_style="red",
|
|
padding=(1, 2),
|
|
)
|
|
console.print(panel, "\n")
|
|
report_error("image_pull_failed", e)
|
|
sys.exit(1)
|
|
|
|
logger.info("%s image %s ready", display_name, image)
|
|
success_text = Text()
|
|
success_text.append(f"{display_name} image ready", style="#22c55e")
|
|
console.print(success_text)
|
|
console.print()
|