fix: v2.5.1 security/ML/reliability patch

Security:
- H1 DNS rebinding TOCTOU: validate_endpoint returns (endpoint, resolved_ips);
  create_pinned_session builds an aiohttp session with a custom resolver that
  returns only the pre-validated IPs, closing the re-resolve gap.
- H2/M3 Shared session cookie pollution: integration no longer uses HA shared
  clientsession; isolated session with DummyCookieJar prevents cross-integration
  cookie leaks.
- Cloud metadata / link-local block added to allow_local_network mode to
  prevent IMDS exfiltration on cloud VMs.
- L3 hard cap extended to history.async_get_history (not only service schema).

ML/LLM correctness:
- _is_openai_reasoning_model uses regex with gpt-5-chat* blacklist and handles
  OpenRouter-style openai/ prefix. Future o5/gpt-6 auto-recognized.
- reasoning_effort=minimal for gpt-5 family, low for o-series.
- Gemini 2.5 Pro gets thinking_budget=128 (Pro rejects 0, flash accepts 0).
- Anthropic extracts first type=text block instead of hardcoded content[0].
- /no_think dedup uses word-boundary regex instead of substring.
- DeepSeek-reasoner detected: skips /no_think; preserves reasoning_content.

Reliability:
- Exception chaining added to Gemini-block re-raises.
- Top-level imports for json/re/hashlib (no more lazy stdlib imports).
- allow_local_network logged at INFO, not WARNING on every setup.

Code style:
- PEP 604 type hints across all .py files (dict/list/| None).

No breaking changes for users. Internal API: validate_endpoint return type
changed from str to tuple[str, list[str]].
This commit is contained in:
SMKRV
2026-04-17 01:58:16 +03:00
parent e8b9b911ef
commit 6e635f7e2f
11 changed files with 367 additions and 204 deletions
+8 -9
View File
@@ -13,7 +13,7 @@ import logging
import os
import re
import traceback
from typing import Any, Dict
from typing import Any
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -21,7 +21,7 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
DEFAULT_METRICS: Dict[str, Any] = {
DEFAULT_METRICS: dict[str, Any] = {
"total_tokens": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
@@ -33,7 +33,6 @@ DEFAULT_METRICS: Dict[str, Any] = {
"min_latency": 0,
}
class MetricsManager:
"""Manages performance metrics for an instance."""
@@ -46,10 +45,10 @@ class MetricsManager:
self.hass = hass
self.instance_name = instance_name
self._metrics_file = metrics_file
self._performance_metrics: Dict[str, Any] = DEFAULT_METRICS.copy()
self._performance_metrics: dict[str, Any] = DEFAULT_METRICS.copy()
@property
def metrics(self) -> Dict[str, Any]:
def metrics(self) -> dict[str, Any]:
return self._performance_metrics
async def async_initialize(self) -> None:
@@ -57,7 +56,7 @@ class MetricsManager:
loaded = await self._load_metrics()
self._performance_metrics = loaded or DEFAULT_METRICS.copy()
async def _load_metrics(self) -> Dict[str, Any] | None:
async def _load_metrics(self) -> dict[str, Any] | None:
try:
exists = await self.hass.async_add_executor_job(
os.path.exists, self._metrics_file
@@ -108,7 +107,7 @@ class MetricsManager:
await self._save_metrics()
async def get_current_metrics(self) -> Dict[str, Any]:
async def get_current_metrics(self) -> dict[str, Any]:
"""Get current performance metrics."""
return self._performance_metrics.copy()
@@ -116,7 +115,7 @@ class MetricsManager:
self,
error: Exception,
model: str,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""Record an error in metrics and return error details."""
self._performance_metrics["total_errors"] += 1
self._performance_metrics["failed_requests"] += 1
@@ -146,7 +145,7 @@ class MetricsManager:
if len(error_msg) > 256:
error_msg = error_msg[:256] + "..."
error_details: Dict[str, Any] = {
error_details: dict[str, Any] = {
"timestamp": dt_util.utcnow().isoformat(),
"model": model,
"instance": self.instance_name,