Guardian-Forge / app /main.py
CreamyClouds's picture
fix: Pass user_id to manager when retrying rejected tools
78fbb32
"""
Guardian Forge Main Application
Entry point for the FastAPI application with proper lifecycle management.
"""
import sys
import asyncio
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from fastapi import FastAPI, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from redis.asyncio import Redis
from pathlib import Path
# Fix for Windows asyncio subprocess issue with Playwright
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
from app.config import settings
from app.core.redis import (
init_redis_pool,
close_redis_pool,
get_redis,
check_redis_health
)
from app.core.logging import setup_logging, get_logger
from app.core.exceptions import GuardianForgeError, exception_to_mcp_error
# Setup logging before anything else
setup_logging()
logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""
Application lifespan context manager.
Handles startup and shutdown events:
- Startup: Initialize Redis connection pool, logging
- Shutdown: Close Redis connection pool, cleanup
"""
# Startup
logger.info(
"Guardian Forge starting",
extra={
"version": settings.app_version,
"environment": settings.environment,
"llm_provider": settings.llm_provider.value
}
)
try:
# Initialize Redis connection pool
await init_redis_pool()
# NOTE: Global project migration disabled after user isolation implementation
# Each user now has their own isolated namespace (user:{user_id}:approved_tools, etc.)
# The old global migration operated on global "approved_tools" which is no longer used
# from app.core.redis import get_redis_client
# from app.core.redis_projects import migrate_existing_tools
#
# redis = await get_redis_client()
# try:
# stats = await migrate_existing_tools(redis)
# logger.info(
# "Project migration completed",
# extra=stats
# )
# except Exception as e:
# logger.warning(f"Migration already completed or failed: {e}")
logger.info("Application startup complete")
except Exception as e:
logger.error(f"Failed to start application: {e}")
raise
yield
# Shutdown
logger.info("Guardian Forge shutting down")
try:
await close_redis_pool()
logger.info("Application shutdown complete")
except Exception as e:
logger.error(f"Error during shutdown: {e}")
# Create FastAPI application
app = FastAPI(
title="Guardian Forge",
description=(
"Autonomous self-extending MCP server with AI-powered security auditing. "
"Dynamically generates, audits, and deploys tools on-demand."
),
version=settings.app_version,
lifespan=lifespan,
docs_url="/docs" if settings.debug else None,
redoc_url="/redoc" if settings.debug else None
)
# CORS middleware (configure appropriately for production)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if settings.debug else [],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ===================================
# Session Validation Middleware (for /admin routes)
# ===================================
@app.middleware("http")
async def session_validation_middleware(request: Request, call_next):
"""
Validate OAuth session for /admin routes on HuggingFace Spaces.
This middleware checks for a valid session_token cookie before
allowing access to the Gradio dashboard.
"""
import os
is_huggingface_space = os.getenv("SPACE_ID") is not None
# Only enforce on /admin routes on HuggingFace Spaces
if is_huggingface_space and request.url.path.startswith("/admin"):
# Allow localhost requests (Gradio's internal calls)
if request.client and request.client.host in ["127.0.0.1", "localhost"]:
response = await call_next(request)
return response
# Check for session cookie
session_token = request.cookies.get("session_token")
if not session_token:
# No session cookie - redirect to login (only for HTML requests)
if "text/html" in request.headers.get("accept", ""):
from fastapi.responses import RedirectResponse
logger.warning(f"Unauthorized /admin access attempt from {request.client.host if request.client else 'unknown'}")
return RedirectResponse(url="/?error=login_required", status_code=302)
else:
# For API requests, return 401
from fastapi.responses import JSONResponse
return JSONResponse(status_code=401, content={"error": "Authentication required"})
# Validate session
from app.core.oauth import get_session
session_data = await get_session(session_token)
if not session_data:
# Invalid/expired session
if "text/html" in request.headers.get("accept", ""):
from fastapi.responses import RedirectResponse
logger.warning(f"Invalid session token for /admin access from {request.client.host if request.client else 'unknown'}")
return RedirectResponse(url="/?error=session_expired", status_code=302)
else:
from fastapi.responses import JSONResponse
return JSONResponse(status_code=401, content={"error": "Session expired"})
# Session valid - attach user info to request state for Gradio callbacks
request.state.user_email = session_data["user_email"]
request.state.user_name = session_data["user_name"]
request.state.user_id = session_data["user_id"]
response = await call_next(request)
return response
# ===================================
# Static Files
# ===================================
# Create static directory if it doesn't exist
STATIC_DIR = Path("./static")
STATIC_DIR.mkdir(exist_ok=True)
# Mount static files directory for HTML artifacts
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# ===================================
# Include Routers
# ===================================
from app.routes.mcp_streamable import router as mcp_streamable_router
from app.routes.oauth import router as oauth_router
app.include_router(mcp_streamable_router) # MCP Streamable HTTP transport
app.include_router(oauth_router) # OAuth authentication routes
# ===================================
# Global Exception Handler
# ===================================
@app.exception_handler(GuardianForgeError)
async def guardian_forge_exception_handler(request, exc: GuardianForgeError):
"""Handle Guardian Forge custom exceptions."""
error_code, error_message = exception_to_mcp_error(exc)
logger.error(
f"Application error: {exc.message}",
extra={
"error_code": exc.error_code,
"details": exc.details
}
)
return JSONResponse(
status_code=500,
content={
"error": {
"code": error_code,
"message": error_message,
"details": exc.details
}
}
)
@app.exception_handler(Exception)
async def general_exception_handler(request, exc: Exception):
"""Handle unexpected exceptions."""
logger.error(f"Unexpected error: {exc}", exc_info=True)
return JSONResponse(
status_code=500,
content={
"error": {
"code": -32603, # Internal error
"message": "Internal server error",
"details": {"type": type(exc).__name__}
}
}
)
# ===================================
# Health Check Endpoints
# ===================================
@app.get("/health")
async def health_check():
"""
Basic health check endpoint.
Returns:
200 OK if application is running
"""
return {
"status": "healthy",
"version": settings.app_version,
"environment": settings.environment
}
@app.get("/health/redis")
async def redis_health_check(redis: Redis = Depends(get_redis)):
"""
Redis connection health check.
Returns:
200 OK if Redis is accessible
503 Service Unavailable if Redis is down
"""
is_healthy = await check_redis_health(redis)
if is_healthy:
return {
"status": "healthy",
"redis_url": settings.redis_host
}
else:
return JSONResponse(
status_code=503,
content={
"status": "unhealthy",
"redis_url": settings.redis_host
}
)
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
"""Root endpoint - landing page with authentication options."""
from app.core.oauth import oauth_config, get_current_user
# Check if user is already authenticated
user = await get_current_user(request)
if user:
# User is authenticated via OAuth - show dashboard link
return HTMLResponse(content=f"""
<!DOCTYPE html>
<html>
<head>
<title>Guardian Forge</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap');
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
max-width: 900px;
margin: 0 auto;
padding: 40px 20px;
background: #000000;
min-height: 100vh;
color: #FFFFFF;
position: relative;
}}
/* Terminal scanline effect */
body::before {{
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 255, 0, 0.03) 0px,
rgba(0, 255, 0, 0.03) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}}
.container {{
background: rgba(0, 0, 0, 0.9);
border: 2px solid #00FF00;
border-radius: 4px;
padding: 40px;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
color: #FFFFFF;
position: relative;
z-index: 2;
}}
h1 {{
color: #00FF00;
margin-bottom: 10px;
font-size: 32px;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
}}
.tagline {{
color: #FFFFFF;
font-size: 16px;
margin-bottom: 30px;
}}
.btn {{
display: inline-block;
padding: 12px 24px;
margin: 10px 10px 10px 0;
border-radius: 4px;
text-decoration: none;
font-weight: 600;
font-family: 'JetBrains Mono', monospace;
transition: all 0.3s ease;
}}
.btn-primary {{
background: #00FF00;
color: #000000;
border: 2px solid #39FF14;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
box-shadow: 0 0 20px rgba(0, 255, 0, 0.5);
}}
.btn-primary:hover {{
background: #39FF14;
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(57, 255, 20, 0.8);
}}
.btn-secondary {{
background: #FF6600;
color: #000000;
border: 2px solid #FF8C00;
text-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
box-shadow: 0 0 20px rgba(255, 102, 0, 0.5);
}}
.btn-secondary:hover {{
background: #FF8C00;
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(255, 140, 0, 0.8);
}}
.user-info {{
background: rgba(0, 255, 0, 0.1);
border-left: 4px solid #00FF00;
padding: 15px;
margin: 20px 0;
border-radius: 4px;
}}
.feature-list {{
list-style: none;
padding: 0;
margin: 30px 0;
}}
.feature-list li {{
padding: 10px 0;
border-bottom: 1px solid rgba(0, 255, 0, 0.2);
color: #FFFFFF;
}}
.feature-list li:before {{
content: "> ";
color: #00FF00;
font-weight: bold;
margin-right: 10px;
}}
hr {{
margin: 30px 0;
border: none;
border-top: 1px solid rgba(0, 255, 0, 0.3);
}}
h3 {{
color: #FF6600;
text-shadow: 0 0 10px rgba(255, 102, 0, 0.8);
margin-bottom: 15px;
}}
.config-box {{
background: rgba(0, 0, 0, 0.5);
border: 1px solid #00FF00;
padding: 20px;
border-radius: 4px;
margin: 20px 0;
}}
code, pre {{
font-family: 'JetBrains Mono', 'Courier New', monospace;
background: rgba(0, 0, 0, 0.7);
color: #00FF00;
padding: 10px;
display: block;
border: 1px solid rgba(0, 255, 0, 0.3);
border-radius: 4px;
margin: 10px 0;
overflow-x: auto;
}}
a {{
color: #00FF00;
text-decoration: none;
}}
a:hover {{
color: #39FF14;
text-decoration: underline;
}}
ul {{
list-style: none;
padding: 0;
color: #FFFFFF;
}}
ul li {{
margin: 8px 0;
}}
strong {{
color: #FF6600;
}}
</style>
</head>
<body>
<div class="container">
<h1>🛡️ Guardian Forge</h1>
<p class="tagline">$ Autonomous self-extending MCP server with AI-powered security</p>
<div class="user-info">
<strong>USER@GUARDIAN-FORGE:~$</strong> Welcome, {user['user_name']}!<br>
<small style="color: #AAAAAA;">{user['user_email']}</small>
</div>
<p style="margin: 20px 0;">SYSTEM STATUS: <span style="color: #00FF00;">AUTHENTICATED</span></p>
<a href="/admin" target="_blank" class="btn btn-primary">Open Dashboard</a>
<a href="/auth/logout" class="btn btn-secondary">Sign Out</a>
<ul class="feature-list">
<li><strong>Generate MCP API Keys</strong> - Create personal API keys for GitHub Copilot</li>
<li><strong>Request New Tools</strong> - AI agents build tools on-demand</li>
<li><strong>Review & Approve</strong> - Human-in-the-loop security review</li>
<li><strong>Private & Isolated</strong> - Your tools and API keys stay completely private</li>
</ul>
<hr>
<h3>$ Connect from Claude Code / Cursor / Copilot</h3>
<div class="config-box">
<p><strong>MCP Server URL:</strong></p>
<code>https://mcp-1st-birthday-guardian-forge.hf.space/mcp</code>
<p style="margin-top: 15px;"><strong>Your API Key:</strong> Generate in the <a href="/admin" target="_blank">Dashboard</a></p>
<p style="margin-top: 15px; font-size: 14px; color: #AAAAAA;">
<strong>GitHub Copilot Config (.vscode/mcp.json):</strong>
</p>
<pre style="font-size: 12px;">{{"servers": {{"guardian-forge": {{"url": "https://mcp-1st-birthday-guardian-forge.hf.space/mcp", "transport": "sse", "headers": {{"Authorization": "Bearer YOUR_API_KEY_HERE"}}}}}}}}</pre>
</div>
<hr>
<h3>$ System Information</h3>
<ul>
<li><strong>Version:</strong> {settings.app_version}</li>
<li><strong>Sandbox:</strong> Modal.com (HuggingFace Spaces)</li>
<li><strong>Health Check:</strong> <a href="/health">/health</a></li>
</ul>
</div>
</body>
</html>
""")
else:
# User not authenticated - show login options
google_oauth_available = oauth_config.is_configured
return HTMLResponse(content=f"""
<!DOCTYPE html>
<html>
<head>
<title>Guardian Forge - MCP 1st Birthday Hackathon</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap');
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
background: #000000;
color: #FFFFFF;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
position: relative;
}}
/* Terminal scanline effect */
body::before {{
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 255, 0, 0.03) 0px,
rgba(0, 255, 0, 0.03) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1;
}}
.container {{
max-width: 650px;
width: 100%;
background: rgba(0, 0, 0, 0.9);
border: 2px solid #00FF00;
border-radius: 4px;
padding: 40px 36px;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
position: relative;
z-index: 2;
}}
.hackathon-badge {{
display: inline-block;
background: #00FF00;
color: #000000;
padding: 6px 14px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 20px;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
box-shadow: 0 0 15px rgba(0, 255, 0, 0.4);
}}
h1 {{
font-size: 36px;
font-weight: 700;
color: #00FF00;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
margin-bottom: 12px;
}}
.tagline {{
color: #FFFFFF;
font-size: 15px;
margin-bottom: 28px;
line-height: 1.6;
}}
.steps {{
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 255, 0, 0.3);
border-radius: 4px;
padding: 20px;
margin: 28px 0;
}}
.step {{
display: flex;
align-items: flex-start;
margin-bottom: 14px;
}}
.step:last-child {{
margin-bottom: 0;
}}
.step-number {{
background: #00FF00;
color: #000000;
width: 24px;
height: 24px;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 13px;
margin-right: 14px;
flex-shrink: 0;
text-shadow: 0 0 5px rgba(0, 255, 0, 0.8);
}}
.step-content {{
color: #FFFFFF;
line-height: 1.6;
font-size: 14px;
}}
.step-content strong {{
color: #FF6600;
text-shadow: 0 0 8px rgba(255, 102, 0, 0.6);
}}
.btn-google {{
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 16px 30px;
background: #00FF00;
color: #000000;
border: 2px solid #39FF14;
border-radius: 4px;
font-size: 15px;
font-weight: 700;
text-decoration: none;
transition: all 0.3s ease;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.5);
margin: 28px 0;
font-family: 'JetBrains Mono', monospace;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.8);
}}
.btn-google:hover {{
background: #39FF14;
transform: translateY(-2px);
box-shadow: 0 0 30px rgba(57, 255, 20, 0.8);
}}
.info-box {{
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 255, 0, 0.3);
border-radius: 4px;
padding: 16px;
margin: 24px 0;
font-size: 14px;
color: #FFFFFF;
}}
.info-box strong {{
color: #FF6600;
text-shadow: 0 0 8px rgba(255, 102, 0, 0.6);
}}
.mcp-url {{
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 255, 0, 0.3);
padding: 10px 14px;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 12px;
color: #00FF00;
word-break: break-all;
margin: 8px 0;
}}
.tech-stack {{
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 102, 0, 0.3);
border-radius: 4px;
padding: 18px;
margin: 24px 0;
}}
.tech-stack-header {{
color: #FF6600;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
text-shadow: 0 0 10px rgba(255, 102, 0, 0.6);
}}
.tech-row {{
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}}
.tech-badge {{
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(0, 255, 0, 0.2);
padding: 5px 12px;
border-radius: 4px;
font-size: 12px;
color: #FFFFFF;
display: flex;
align-items: center;
gap: 5px;
}}
.footer {{
text-align: center;
margin-top: 28px;
padding-top: 20px;
border-top: 1px solid rgba(0, 255, 0, 0.2);
font-size: 12px;
color: #AAAAAA;
}}
.footer a {{
color: #00FF00;
text-decoration: none;
margin: 0 6px;
}}
.footer a:hover {{
text-decoration: underline;
color: #39FF14;
}}
.footer strong {{
color: #FF6600;
}}
.warning {{
background: rgba(255, 102, 0, 0.1);
border: 1px solid rgba(255, 102, 0, 0.3);
border-radius: 4px;
padding: 16px;
margin: 24px 0;
color: #FF6600;
}}
@media (max-width: 640px) {{
.container {{
padding: 28px 20px;
}}
h1 {{
font-size: 28px;
}}
}}
</style>
</head>
<body>
<div class="container">
<div class="hackathon-badge">🎂 MCP 1st Birthday Hackathon</div>
<h1>🛡️ Guardian Forge</h1>
<p class="tagline">
$ Build custom AI tools on-demand with automated security auditing.<br>
$ Sign in to start creating!
</p>
{'<a href="https://mcp-1st-birthday-guardian-forge.hf.space/auth/login" target="_blank" class="btn-google"><svg width="20" height="20" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>Sign in with Google</a>' if google_oauth_available else '<div class="warning">⚠️ Google OAuth is not configured. Contact the administrator to set up authentication.</div>'}
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<strong>Sign in</strong> with your Google account to get started
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<strong>Generate an API key</strong> from the Settings tab in your dashboard
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<strong>Connect GitHub Copilot or Claude Desktop</strong> using the MCP server URL below
</div>
</div>
</div>
<div class="info-box">
<strong>MCP Server URL:</strong>
<div class="mcp-url">https://mcp-1st-birthday-guardian-forge.hf.space/mcp</div>
<small style="color: #AAAAAA;">Use this URL in GitHub Copilot, Claude Desktop, Cursor, or any MCP-compatible client</small>
</div>
<div class="tech-stack">
<div class="tech-stack-header">$ Powered By</div>
<div class="tech-row">
<div class="tech-badge">
<span>🎨</span>
<strong>Gradio</strong>
</div>
<div class="tech-badge">
<span>⚡</span>
<strong>FastAPI</strong>
</div>
<div class="tech-badge">
<span>🗄️</span>
<strong>Redis</strong>
</div>
<div class="tech-badge">
<span>🤗</span>
<strong>HF Spaces</strong>
</div>
</div>
<div class="tech-row">
<div class="tech-badge">
<span>🦜</span>
<strong>LangChain</strong>
</div>
<div class="tech-badge">
<span>🕸️</span>
<strong>LangGraph</strong>
</div>
<div class="tech-badge">
<span>🧠</span>
<strong>NEBIUS AI</strong>
</div>
</div>
<div class="tech-row">
<div class="tech-badge">
<span>🔐</span>
<strong>Google OAuth</strong>
</div>
<div class="tech-badge">
<span>🛡️</span>
<strong>Bandit SAST</strong>
</div>
<div class="tech-badge">
<span>📦</span>
<strong>Modal.com</strong>
</div>
</div>
<div style="color: #AAAAAA; font-size: 10px; margin-top: 12px; text-align: center;">
AI Agents • Serverless Execution • Enterprise Security
</div>
</div>
<div class="footer">
<div style="margin-bottom: 8px;">
<strong>Version:</strong> {settings.app_version}
</div>
<div>
<a href="https://huggingface.co/MCP-1st-Birthday" target="_blank">Hackathon Page</a>
<span style="color: #333333;">•</span>
<a href="/health">Health</a>
<span style="color: #333333;">•</span>
<a href="https://huggingface.co/spaces/MCP-1st-Birthday/Guardian-Forge/blob/main/LICENSE" target="_blank">MIT License</a>
</div>
</div>
</div>
</body>
</html>
""")
# ===================================
# HTML Artifact Serving
# ===================================
@app.get("/artifacts/{artifact_id}", response_class=HTMLResponse)
async def serve_artifact(artifact_id: str, redis: Redis = Depends(get_redis)):
"""
Serve HTML artifacts for chat interface previews.
Args:
artifact_id: Unique identifier for the HTML artifact
redis: Redis connection dependency
Returns:
HTML content for the artifact
"""
try:
# Retrieve HTML content from Redis
html_content = await redis.get(f"artifact:{artifact_id}")
if html_content is None:
return HTMLResponse(
content="<html><body><h1>Artifact Not Found</h1><p>This artifact may have expired or does not exist.</p></body></html>",
status_code=404
)
# Return the HTML content
return HTMLResponse(content=html_content.decode('utf-8'))
except Exception as e:
logger.error(f"Error serving artifact {artifact_id}: {str(e)}")
return HTMLResponse(
content=f"<html><body><h1>Error</h1><p>Failed to load artifact: {str(e)}</p></body></html>",
status_code=500
)
# ===================================
# Gradio Dashboard Mount
# ===================================
# Mount Gradio dashboard at /admin
try:
from app.dashboard.gradio_app import create_gradio_interface
from app.core.oauth import get_session
import gradio as gr
gradio_app = create_gradio_interface()
# Custom authentication function that supports both OAuth and basic auth
async def gradio_auth(request):
"""
Authenticate Gradio requests using either:
1. OAuth session cookie (production with Google OAuth)
2. Basic auth credentials (local development)
Returns username if authenticated, None otherwise.
"""
# Try OAuth session first
session_token = request.cookies.get("session_token")
if session_token:
session_data = await get_session(session_token)
if session_data:
# Return email as username for OAuth users
return session_data["user_email"]
# Fall back to basic auth if OAuth not configured or session invalid
if settings.gradio_username and settings.gradio_password:
# Gradio will handle basic auth automatically
return None # Let Gradio's basic auth handle it
# No authentication configured
return "demo_user@guardian-forge.local"
# Mount with authentication
# On HuggingFace Spaces, disable basic auth (doesn't work with HF's reverse proxy)
# Users must authenticate via Google OAuth at root level first
import os
is_huggingface_space = os.getenv("SPACE_ID") is not None
if is_huggingface_space:
# HuggingFace Spaces - mount without Gradio auth (use FastAPI middleware instead)
# Session validation happens in the /admin endpoint before reaching Gradio
app = gr.mount_gradio_app(app, gradio_app, path="/admin")
logger.info("Gradio dashboard mounted at /admin (HuggingFace Spaces - session validation via FastAPI)")
elif settings.gradio_username and settings.gradio_password:
# Local deployment - use basic auth as fallback
auth_tuple = (settings.gradio_username, settings.gradio_password)
app = gr.mount_gradio_app(app, gradio_app, path="/admin", auth=auth_tuple)
logger.info("Gradio dashboard mounted at /admin with basic auth (fallback for OAuth)")
else:
app = gr.mount_gradio_app(app, gradio_app, path="/admin")
logger.warning("Gradio dashboard mounted at /admin WITHOUT authentication - configure GRADIO_USERNAME and GRADIO_PASSWORD or enable Google OAuth")
except Exception as e:
logger.error(f"Failed to mount Gradio dashboard: {e}")
logger.warning("Continuing without dashboard - install gradio to enable")
# ===================================
# Main Entry Point
# ===================================
def main() -> None:
"""
Main entry point for running Guardian Forge.
This function is used by the CLI command defined in pyproject.toml.
For development, use: uvicorn app.main:app --reload
For production, use: gunicorn app.main:app -k uvicorn.workers.UvicornWorker
"""
import uvicorn
uvicorn.run(
"app.main:app",
host=settings.host,
port=settings.port,
reload=settings.reload,
log_level=settings.log_level.value.lower(),
access_log=True
)
if __name__ == "__main__":
main()