agromatrix: enforce mentor auth and expose shared-memory review via gateway

This commit is contained in:
NODA1 System
2026-02-21 13:17:59 +01:00
parent 68ac8fa355
commit f44e920486
4 changed files with 152 additions and 7 deletions

View File

@@ -1,10 +1,12 @@
import asyncio
from datetime import datetime, timezone
import hmac
import json
import os
import uuid
from typing import Any, Dict, List
import httpx
from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
@@ -17,6 +19,14 @@ router = APIRouter(prefix="/v1", tags=["daarion-facade"])
EVENT_TERMINAL_STATUSES = {"done", "failed"}
EVENT_KNOWN_STATUSES = {"queued", "running", "done", "failed"}
EVENT_POLL_SECONDS = float(os.getenv("DAARION_JOB_EVENTS_POLL_SECONDS", "0.5"))
ROUTER_URL = os.getenv("ROUTER_URL", "http://router:8000").rstrip("/")
ROUTER_REVIEW_TIMEOUT = float(os.getenv("DAARION_ROUTER_REVIEW_TIMEOUT_SECONDS", "20"))
AGROMATRIX_REVIEW_AUTH_MODE = os.getenv("AGROMATRIX_REVIEW_AUTH_MODE", "bearer").strip().lower()
AGROMATRIX_REVIEW_BEARER_TOKENS = [
part.strip()
for part in os.getenv("AGROMATRIX_REVIEW_BEARER_TOKENS", "").replace(";", ",").split(",")
if part.strip()
]
class InvokeInput(BaseModel):
@@ -36,6 +46,69 @@ class InvokeResponse(BaseModel):
status_url: str
class SharedMemoryReviewRequest(BaseModel):
point_id: str
approve: bool
reviewer: str | None = None
note: str | None = None
def _extract_bearer_token(request: Request) -> str:
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing Bearer token")
token = auth_header[len("Bearer ") :].strip()
if not token:
raise HTTPException(status_code=401, detail="Empty Bearer token")
return token
def _require_mentor_auth(request: Request) -> str:
mode = AGROMATRIX_REVIEW_AUTH_MODE
if mode in {"off", "none", "disabled"}:
return ""
if mode != "bearer":
raise HTTPException(status_code=500, detail=f"Unsupported AGROMATRIX_REVIEW_AUTH_MODE={mode}")
if not AGROMATRIX_REVIEW_BEARER_TOKENS:
raise HTTPException(status_code=503, detail="Review auth is not configured")
token = _extract_bearer_token(request)
if not any(hmac.compare_digest(token, candidate) for candidate in AGROMATRIX_REVIEW_BEARER_TOKENS):
raise HTTPException(status_code=403, detail="Invalid mentor token")
return token
async def _router_json(
method: str,
path: str,
*,
payload: Dict[str, Any] | None = None,
params: Dict[str, Any] | None = None,
authorization: str | None = None,
) -> Dict[str, Any]:
headers: Dict[str, str] = {}
if authorization:
headers["Authorization"] = authorization
url = f"{ROUTER_URL}{path}"
try:
async with httpx.AsyncClient(timeout=ROUTER_REVIEW_TIMEOUT) as client:
resp = await client.request(method, url, json=payload, params=params, headers=headers)
except httpx.TimeoutException:
raise HTTPException(status_code=504, detail="Router timeout")
except Exception as e:
raise HTTPException(status_code=502, detail=f"Router unavailable: {e}")
try:
body = resp.json()
except Exception:
body = {"raw": resp.text}
if resp.status_code >= 400:
detail = body.get("detail") if isinstance(body, dict) else body
raise HTTPException(status_code=resp.status_code, detail=detail or f"Router error {resp.status_code}")
return body if isinstance(body, dict) else {"data": body}
def _sse_message(event: str, payload: Dict[str, Any]) -> str:
return f"event: {event}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
@@ -116,3 +189,24 @@ async def job_events(job_id: str, request: Request) -> StreamingResponse:
"X-Accel-Buffering": "no",
},
)
@router.get("/agromatrix/shared-memory/pending")
async def agromatrix_shared_pending(limit: int = 50) -> Dict[str, Any]:
return await _router_json(
"GET",
"/v1/agromatrix/shared-memory/pending",
params={"limit": max(1, min(limit, 200))},
)
@router.post("/agromatrix/shared-memory/review")
async def agromatrix_shared_review(req: SharedMemoryReviewRequest, request: Request) -> Dict[str, Any]:
token = _require_mentor_auth(request)
auth_header = f"Bearer {token}" if token else None
return await _router_json(
"POST",
"/v1/agromatrix/shared-memory/review",
payload=req.model_dump(),
authorization=auth_header,
)