strix/strix/interface/environment.py
B661LP ad0a371d80 refactor(runtime): rename docker-specific helpers to backend-agnostic names
- 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
2026-09-09 14:11:23 +02:00

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()