feat(cg1+cg2+cg3): billing endpoints, IAP middleware, static serving

This commit is contained in:
chrischristiansen-glitch 2026-05-27 19:20:45 +02:00
parent c79b7d87b4
commit 324867e289

81
main.py
View File

@ -3,26 +3,78 @@
main.py Cloud Run entrypoint, OSVauco OPAX. main.py Cloud Run entrypoint, OSVauco OPAX.
Modes: light (gemini-2.5-flash) | heavy (gemini-2.5-pro) Modes: light (gemini-2.5-flash) | heavy (gemini-2.5-pro)
ML-1: telemetri, state store, DAG. ML-1: telemetri, state store, DAG.
CG1+CG2: billing endpoints + IAP-beskyttelse.
CG3: static-mappe serveres fra /static/*.
""" """
import os import os
import sys import sys
import time import time
import pathlib
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "agents", "core-logic")) 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 pydantic import BaseModel, Field
from typing import List from typing import List
from agent import run, authorize_mode from agent import run, authorize_mode
from ml import build_agent_dag, execute_dag, get_store, log_agent_call from ml import build_agent_dag, execute_dag, get_store, log_agent_call
from ml.telemetry import log_dag_execution 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") app = FastAPI(title="OSVauco OPAX Agent")
AGENT_ID = "opax-core" 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): class RunRequest(BaseModel):
message: str message: str
user_id: str = "opax" user_id: str = "opax"
@ -38,11 +90,38 @@ class DagRequest(BaseModel):
scheduler: str = Field("threads") scheduler: str = Field("threads")
# ── HEALTH ────────────────────────────────────────────────────────────────────
@app.get("/health") @app.get("/health")
def health(): def health():
return {"status": "ok"} 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") @app.post("/run")
def run_agent(req: RunRequest): def run_agent(req: RunRequest):
try: try: