diff --git a/main.py b/main.py index 6835101..7602bb8 100644 --- a/main.py +++ b/main.py @@ -3,26 +3,78 @@ main.py — Cloud Run entrypoint, OSVauco OPAX. Modes: light (gemini-2.5-flash) | heavy (gemini-2.5-pro) ML-1: telemetri, state store, DAG. +CG1+CG2: billing endpoints + IAP-beskyttelse. +CG3: static-mappe serveres fra /static/*. """ import os import sys import time +import pathlib sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agents", "core-logic")) -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from typing import List from agent import run, authorize_mode from ml import build_agent_dag, execute_dag, get_store, log_agent_call from ml.telemetry import log_dag_execution +from ml.billing_agent import BillingAgent +from ml.anomaly_detector import AnomalyDetector app = FastAPI(title="OSVauco OPAX Agent") AGENT_ID = "opax-core" +# ── IAP-MIDDLEWARE ──────────────────────────────────────────────────────────── +# Cloud Run + IAP: verifiserer X-Goog-IAP-JWT-Assertion på /billing/*. +# Sett env-variabelen IAP_AUDIENCE til IAP backend service audience: +# /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID +# Sett IAP_ENABLED=true i Cloud Run for å aktivere sjekken. +# Lokalt (IAP_ENABLED ikke satt): alle kall tillates. +IAP_ENABLED = os.environ.get("IAP_ENABLED", "").lower() == "true" +IAP_AUDIENCE = os.environ.get("IAP_AUDIENCE", "") + + +def _verify_iap_jwt(token: str, audience: str) -> dict: + """Verifiserer IAP JWT med google-auth. Kaster ValueError ved feil.""" + try: + from google.auth.transport import requests as google_requests + from google.oauth2 import id_token + request = google_requests.Request() + return id_token.verify_token(token, request, audience=audience, + certs_url="https://www.gstatic.com/iap/verify/public_key") + except Exception as exc: + raise ValueError(f"IAP JWT ugyldig: {exc}") from exc + + +@app.middleware("http") +async def iap_guard(request: Request, call_next): + """Blokkerer /billing/* uten gyldig IAP-token når IAP_ENABLED=true.""" + if IAP_ENABLED and request.url.path.startswith("/billing"): + token = request.headers.get("X-Goog-IAP-JWT-Assertion", "") + if not token: + return JSONResponse(status_code=401, + content={"error": "IAP-token mangler (X-Goog-IAP-JWT-Assertion)"}) + try: + _verify_iap_jwt(token, IAP_AUDIENCE) + except ValueError as exc: + return JSONResponse(status_code=403, content={"error": str(exc)}) + return await call_next(request) + + +# ── STATIC FILES ────────────────────────────────────────────────────────────── +# Serverer static/ direkte: GET /static/billing-dashboard.html osv. +_static_dir = pathlib.Path(__file__).parent / "static" +if _static_dir.is_dir(): + app.mount("/static", StaticFiles(directory=str(_static_dir), html=True), name="static") + + +# ── MODELLER ────────────────────────────────────────────────────────────────── class RunRequest(BaseModel): message: str user_id: str = "opax" @@ -38,11 +90,38 @@ class DagRequest(BaseModel): scheduler: str = Field("threads") +# ── HEALTH ──────────────────────────────────────────────────────────────────── @app.get("/health") def health(): return {"status": "ok"} +# ── BILLING ENDPOINTS (CG1 + CG2) ──────────────────────────────────────────── +@app.get("/billing/summary") +def billing_summary(): + try: + return BillingAgent().get_summary() + except Exception as exc: + return JSONResponse(status_code=500, content={"error": str(exc)}) + + +@app.get("/billing/forecast") +def billing_forecast(): + try: + return BillingAgent().get_forecast() + except Exception as exc: + return JSONResponse(status_code=500, content={"error": str(exc)}) + + +@app.get("/billing/anomalies") +def billing_anomalies(): + try: + return AnomalyDetector().detect_anomalies() + except Exception as exc: + return JSONResponse(status_code=500, content={"error": str(exc)}) + + +# ── AGENT ENDPOINTS ─────────────────────────────────────────────────────────── @app.post("/run") def run_agent(req: RunRequest): try: