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:
@@ -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
|
||||
|
||||
+19
-5
@@ -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,13 +96,19 @@ 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():
|
||||
return FileResponse(target)
|
||||
|
||||
# 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():
|
||||
# index.html nie cachen → Browser zieht nach jedem Deploy das aktuelle (gehashte) Bundle.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
@@ -97,20 +96,19 @@ 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 for chunk in r.aiter_raw():
|
||||
record_stream_chunk(chunk, alias)
|
||||
yield chunk
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@router.post("/chat/completions")
|
||||
|
||||
+21
-12
@@ -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:
|
||||
@@ -164,18 +166,25 @@ def check_versions_cached() -> dict:
|
||||
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(),
|
||||
"hermes_ui": get_git_info("~/hermes-webui"),
|
||||
"hermes_agent": find_hermes_agent_git()
|
||||
}
|
||||
_VERSION_CACHE["ts"] = now
|
||||
_VERSION_CACHE["data"] = data
|
||||
return data
|
||||
|
||||
# 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(),
|
||||
"hermes_ui": get_git_info("~/hermes-webui"),
|
||||
"hermes_agent": find_hermes_agent_git()
|
||||
}
|
||||
_VERSION_CACHE["ts"] = now
|
||||
_VERSION_CACHE["data"] = data
|
||||
return data
|
||||
|
||||
|
||||
def system_status() -> dict:
|
||||
|
||||
Reference in New Issue
Block a user