Spaces:
Runtime error
Runtime error
| """ | |
| 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__) | |
| 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) | |
| # =================================== | |
| 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 | |
| # =================================== | |
| 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 | |
| } | |
| } | |
| ) | |
| 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 | |
| # =================================== | |
| 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 | |
| } | |
| 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 | |
| } | |
| ) | |
| 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 | |
| # =================================== | |
| 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() | |