Antigravity-Review-Fixes: Traversal-Guard, Client-Pooling, Cache-Lock, Doku

Vier verifizierte Befunde aus dem externen Review (Gemini 3.1 Pro):
- app.py: SPA-Fallback gegen Path-Traversal gehaertet (resolve + is_relative_to,
  liefert nur noch Dateien INNERHALB von frontend/dist aus).
- app.py/gateway_proxy.py: geteilter httpx.AsyncClient im lifespan statt neuer
  Client pro /v1-Anfrage (Keep-Alive/Pooling, spart Sockets unter parallelen Agent-Stroemen).
- system.py: check_versions_cached() mit threading.Lock + Double-Check gegen Scan-Stampede.
- AGENTS.md: Zeitzonen-Drift korrigiert (Box laeuft Europe/Berlin, nicht UTC).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-05 10:21:32 +02:00
parent ff1b9a085f
commit 12c387a6de
4 changed files with 51 additions and 30 deletions
+2 -2
View File
@@ -24,8 +24,8 @@ Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadc
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
## Zeit & Umgebung
- **Box = Ubuntu, läuft in UTC.** Dev-PC = Windows. Naive/lokale Zeiten immer über
`MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
(siehe `backend/services/reminders.py`).
## Backend-Konventionen
+17 -3
View File
@@ -11,6 +11,7 @@ import logging
import os
from contextlib import asynccontextmanager
import httpx
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
@@ -44,11 +45,18 @@ async def lifespan(app: FastAPI):
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
# pro /v1-Anfrage (spart Sockets/TIME_WAIT unter parallelen Agent-Strömen von Zed/Kilo).
app.state.gw_client = httpx.AsyncClient(
timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=10.0),
limits=httpx.Limits(max_keepalive_connections=32, max_connections=64),
)
try:
yield
finally:
for task in tasks:
task.cancel()
await app.state.gw_client.aclose()
app = FastAPI(title="Mission Control 2.0", version=VERSION, lifespan=lifespan)
@@ -88,12 +96,18 @@ app.include_router(console.router) # Box-Konsole (ttyd) same-origin durchreiche
if FRONTEND_DIST.exists():
app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets")
_DIST_ROOT = FRONTEND_DIST.resolve()
@app.get("/{full_path:path}")
def spa(full_path: str):
# Falls die Datei direkt in FRONTEND_DIST liegt (z.B. manifest.webmanifest, favicon.ico), liefere sie aus
target = FRONTEND_DIST / full_path
if target.is_file():
# Datei direkt aus FRONTEND_DIST ausliefern (manifest.webmanifest, favicon.ico, …) — aber
# NUR innerhalb des dist-Ordners: Pfad auflösen + Traversal (../, absolute Pfade) hart raus.
try:
target = (FRONTEND_DIST / full_path).resolve()
if target.is_relative_to(_DIST_ROOT) and target.is_file():
return FileResponse(target)
except (ValueError, OSError):
pass
index = FRONTEND_DIST / "index.html"
if index.exists():
+3 -5
View File
@@ -1,6 +1,5 @@
import os
import httpx
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse, StreamingResponse
@@ -97,17 +96,16 @@ async def _proxy(path: str, request: Request):
_inject_language(body, alias)
url = f"{LLAMA_SWAP_URL}{path}"
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
if body.get("stream"):
async def gen():
async with httpx.AsyncClient(timeout=None) as c:
async with c.stream("POST", url, json=body) as r:
async with client.stream("POST", url, json=body, timeout=None) as r:
async for chunk in r.aiter_raw():
record_stream_chunk(chunk, alias)
yield chunk
return StreamingResponse(gen(), media_type="text/event-stream", headers=routed)
async with httpx.AsyncClient(timeout=600) as c:
r = await c.post(url, json=body)
r = await client.post(url, json=body, timeout=600.0)
resp_json = r.json()
record_usage(resp_json.get("usage") if isinstance(resp_json, dict) else None, alias)
return JSONResponse(resp_json, status_code=r.status_code, headers=routed)
+10 -1
View File
@@ -9,6 +9,7 @@ der Box, daher sysfs). Verschachtelte Struktur wie v1 (cpu.percent, ram.used Byt
import glob
import os
import subprocess
import threading
import psutil
@@ -157,6 +158,7 @@ def get_engine_version() -> dict:
_VERSION_CACHE = {"ts": 0.0, "data": {}}
_VERSION_LOCK = threading.Lock()
def check_versions_cached() -> dict:
@@ -165,8 +167,15 @@ def check_versions_cached() -> dict:
if now - _VERSION_CACHE["ts"] < 30.0:
return _VERSION_CACHE["data"]
mc2_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
# Lock gegen Scan-Stampede: FastAPI führt sync-Routen im Threadpool aus → ohne Lock würden
# parallele Dashboard-/Agent-Aufrufe die git-/Versions-Scans mehrfach gleichzeitig anwerfen.
# Doppelt geprüft, damit ein zweiter Thread den frisch gefüllten Cache nimmt statt neu zu scannen.
with _VERSION_LOCK:
now = time.time()
if now - _VERSION_CACHE["ts"] < 30.0:
return _VERSION_CACHE["data"]
mc2_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
data = {
"mc2": get_git_info(mc2_path),
"engine": get_engine_version(),