Files
stack/apps/coordinator/src/main.py
T
Jason WoltjeandClaude Sonnet 4.5 5d683d401e fix(#121): Remediate security issues from ORCH-121 review
Priority Fixes (Required Before Production):

H3: Add rate limiting to webhook endpoint
- Added slowapi library for FastAPI rate limiting
- Implemented per-IP rate limiting (100 req/min) on webhook endpoint
- Added global rate limiting support via slowapi

M4: Add subprocess timeouts to all gates
- Added timeout=300 (5 minutes) to all subprocess.run() calls in gates
- Implemented proper TimeoutExpired exception handling
- Removed dead CalledProcessError handlers (check=False makes them unreachable)

M2: Add input validation on QualityCheckRequest
- Validate files array size (max 1000 files)
- Validate file paths (no path traversal, no null bytes, no absolute paths)
- Validate diff summary size (max 10KB)
- Validate taskId and agentId format (non-empty)

Additional Fixes:

H1: Fix coverage.json path resolution
- Use absolute paths resolved from project root
- Validate path is within project boundaries (prevent path traversal)

Code Review Cleanup:
- Moved imports to module level in quality_orchestrator.py
- Refactored mock detection logic into separate helper methods
- Removed dead subprocess.CalledProcessError exception handlers from all gates

Testing:
- Added comprehensive tests for all security fixes
- All 339 coordinator tests pass
- All 447 orchestrator tests pass
- Followed TDD principles (RED-GREEN-REFACTOR)

Security Impact:
- Prevents webhook DoS attacks via rate limiting
- Prevents hung processes via subprocess timeouts
- Prevents path traversal attacks via input validation
- Prevents malformed input attacks via comprehensive validation

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
2026-02-04 11:50:05 -06:00

171 lines
4.7 KiB
Python

"""FastAPI application for mosaic-coordinator webhook receiver."""
import asyncio
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
from fastapi import FastAPI, Request
from pydantic import BaseModel
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
from .config import settings
from .coordinator import Coordinator
from .queue import QueueManager
from .webhook import router as webhook_router
# Configure logging
def setup_logging() -> None:
"""Configure logging for the application."""
log_level = getattr(logging, settings.log_level.upper(), logging.INFO)
logging.basicConfig(
level=log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
# Setup logging on module import
setup_logging()
logger = logging.getLogger(__name__)
# Global instances for application state
_coordinator: Coordinator | None = None
_coordinator_task: asyncio.Task[None] | None = None
def get_coordinator() -> Coordinator | None:
"""Get the global coordinator instance.
Returns:
The Coordinator instance if initialized, None otherwise
"""
return _coordinator
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, Any]]:
"""Application lifespan manager.
Handles startup and shutdown logic including coordinator lifecycle.
Yields:
State dict with shared resources
"""
global _coordinator, _coordinator_task
# Startup
logger.info("Starting mosaic-coordinator webhook receiver")
logger.info(f"Gitea URL: {settings.gitea_url}")
logger.info(f"Log level: {settings.log_level}")
logger.info(f"Server: {settings.host}:{settings.port}")
# Initialize queue manager
queue_file = Path("queue.json")
queue_manager = QueueManager(queue_file=queue_file)
logger.info(f"Queue manager initialized (file: {queue_file})")
# Initialize and start coordinator if enabled
if settings.coordinator_enabled:
_coordinator = Coordinator(
queue_manager=queue_manager,
poll_interval=settings.coordinator_poll_interval,
)
logger.info(
f"Coordinator initialized (poll interval: {settings.coordinator_poll_interval}s, "
f"max agents: {settings.coordinator_max_concurrent_agents})"
)
# Start coordinator in background
_coordinator_task = asyncio.create_task(_coordinator.start())
logger.info("Coordinator orchestration loop started")
else:
logger.info("Coordinator disabled via configuration")
yield {"queue_manager": queue_manager, "coordinator": _coordinator}
# Shutdown
logger.info("Shutting down mosaic-coordinator")
# Stop coordinator gracefully
if _coordinator is not None:
logger.info("Stopping coordinator...")
await _coordinator.stop()
if _coordinator_task is not None:
_coordinator_task.cancel()
try:
await _coordinator_task
except asyncio.CancelledError:
pass
logger.info("Coordinator stopped")
logger.info("Mosaic-coordinator shutdown complete")
# Initialize rate limiter
limiter = Limiter(key_func=get_remote_address)
# Create FastAPI application
app = FastAPI(
title="Mosaic Coordinator",
description="Webhook receiver for Gitea issue events",
version="0.0.1",
lifespan=lifespan,
)
# Register rate limiter
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
class HealthResponse(BaseModel):
"""Health check response model."""
status: str
service: str
coordinator_running: bool = False
active_agents: int = 0
@app.get("/health", response_model=HealthResponse)
async def health_check() -> HealthResponse:
"""Health check endpoint.
Returns:
HealthResponse indicating service is healthy with coordinator status
"""
coordinator_running = False
active_agents = 0
if _coordinator is not None:
coordinator_running = _coordinator.is_running
active_agents = _coordinator.get_active_agent_count()
return HealthResponse(
status="healthy",
service="mosaic-coordinator",
coordinator_running=coordinator_running,
active_agents=active_agents,
)
# Include webhook router
app.include_router(webhook_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"src.main:app",
host=settings.host,
port=settings.port,
reload=True,
log_level=settings.log_level.lower(),
)