from fastapi import FastAPI, Depends, HTTPException, Request, Response, Cookie from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, RedirectResponse, JSONResponse from sqlalchemy.orm import Session from pydantic import BaseModel from datetime import datetime, timezone from typing import Optional import hashlib import hmac import json import os import secrets from database import get_db, init_db from models import Agent, Run app = FastAPI(title="Agent Command Center", version="1.0.0") # --- Auth --- AUTH_USER = os.environ.get("AUTH_USER", "eric") AUTH_PASS = os.environ.get("AUTH_PASS", "Kj8#mPx2vQ!nR4wL") SESSION_SECRET = os.environ.get("SESSION_SECRET", secrets.token_hex(32)) # In-memory session store _sessions: dict[str, str] = {} def create_session(username: str) -> str: token = secrets.token_urlsafe(32) _sessions[token] = username return token def get_current_user(session: Optional[str] = Cookie(None)) -> Optional[str]: if session and session in _sessions: return _sessions[session] return None def require_auth(session: Optional[str] = Cookie(None)): user = get_current_user(session) if not user: raise HTTPException(status_code=401, detail="Not authenticated") return user class LoginRequest(BaseModel): username: str password: str @app.post("/api/login") def login(creds: LoginRequest, response: Response): if creds.username == AUTH_USER and creds.password == AUTH_PASS: token = create_session(creds.username) response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7) return {"status": "ok", "user": creds.username} raise HTTPException(status_code=401, detail="Invalid credentials") @app.post("/api/logout") def logout(response: Response, session: Optional[str] = Cookie(None)): if session and session in _sessions: del _sessions[session] response.delete_cookie("session") return {"status": "ok"} @app.get("/login") def login_page(): return FileResponse("static/login.html") # --- Pydantic schemas --- class AgentCreate(BaseModel): id: str name: str description: str = "" schedule: str = "manual" config: dict = {} class AgentUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None schedule: Optional[str] = None status: Optional[str] = None config: Optional[dict] = None class RunCreate(BaseModel): status: str = "running" output: str = "" error: str = "" metadata: dict = {} class RunUpdate(BaseModel): status: Optional[str] = None output: Optional[str] = None error: Optional[str] = None finished_at: Optional[str] = None metadata: Optional[dict] = None # --- API routes --- @app.get("/api/health") def health(): return {"status": "ok", "service": "agent-command-center"} @app.get("/api/agents/{agent_id}/config") def get_agent_config(agent_id: str, db: Session = Depends(get_db)): """Internal endpoint for agents to fetch their config. No auth required.""" agent = db.query(Agent).filter(Agent.id == agent_id).first() if not agent: raise HTTPException(status_code=404, detail="Agent not found") return agent.config or {} @app.get("/api/agents") def list_agents(user: str = Depends(require_auth), db: Session = Depends(get_db)): agents = db.query(Agent).all() result = [] for a in agents: last_run = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).first() recent_runs = db.query(Run).filter(Run.agent_id == a.id).order_by(Run.started_at.desc()).limit(10).all() success_streak = 0 for r in recent_runs: if r.status == "success": success_streak += 1 else: break result.append({ "id": a.id, "name": a.name, "description": a.description, "schedule": a.schedule, "status": a.status, "config": a.config or {}, "created_at": a.created_at.isoformat() if a.created_at else None, "last_run": { "status": last_run.status, "started_at": last_run.started_at.isoformat() if last_run.started_at else None, "finished_at": last_run.finished_at.isoformat() if last_run.finished_at else None, } if last_run else None, "success_streak": success_streak, "total_runs": db.query(Run).filter(Run.agent_id == a.id).count(), }) return result @app.post("/api/agents") def create_agent(agent: AgentCreate, db: Session = Depends(get_db)): existing = db.query(Agent).filter(Agent.id == agent.id).first() if existing: raise HTTPException(status_code=409, detail="Agent already exists") new_agent = Agent( id=agent.id, name=agent.name, description=agent.description, schedule=agent.schedule, config=agent.config, ) db.add(new_agent) db.commit() return {"id": new_agent.id, "status": "created"} @app.get("/api/agents/{agent_id}") def get_agent(agent_id: str, user: str = Depends(require_auth), db: Session = Depends(get_db)): agent = db.query(Agent).filter(Agent.id == agent_id).first() if not agent: raise HTTPException(status_code=404, detail="Agent not found") runs = db.query(Run).filter(Run.agent_id == agent_id).order_by(Run.started_at.desc()).limit(50).all() return { "id": agent.id, "name": agent.name, "description": agent.description, "schedule": agent.schedule, "status": agent.status, "config": agent.config or {}, "created_at": agent.created_at.isoformat() if agent.created_at else None, "runs": [{ "id": r.id, "started_at": r.started_at.isoformat() if r.started_at else None, "finished_at": r.finished_at.isoformat() if r.finished_at else None, "status": r.status, "output": r.output, "error": r.error, "metadata": r.metadata_, } for r in runs], } @app.put("/api/agents/{agent_id}") def update_agent(agent_id: str, update: AgentUpdate, db: Session = Depends(get_db)): agent = db.query(Agent).filter(Agent.id == agent_id).first() if not agent: raise HTTPException(status_code=404, detail="Agent not found") updates = update.model_dump(exclude_none=True) if "config" in updates: current_config = agent.config or {} current_config.update(updates.pop("config")) agent.config = current_config for field, value in updates.items(): setattr(agent, field, value) db.commit() return {"id": agent.id, "status": "updated"} @app.post("/api/agents/{agent_id}/runs") def create_run(agent_id: str, run: RunCreate, db: Session = Depends(get_db)): agent = db.query(Agent).filter(Agent.id == agent_id).first() if not agent: raise HTTPException(status_code=404, detail="Agent not found") new_run = Run( agent_id=agent_id, status=run.status, output=run.output, error=run.error, metadata_=run.metadata, ) if run.status in ("success", "failed"): new_run.finished_at = datetime.now(timezone.utc) db.add(new_run) db.commit() return {"id": new_run.id, "status": new_run.status} @app.put("/api/runs/{run_id}") def update_run(run_id: int, update: RunUpdate, db: Session = Depends(get_db)): run = db.query(Run).filter(Run.id == run_id).first() if not run: raise HTTPException(status_code=404, detail="Run not found") if update.status is not None: run.status = update.status if update.output is not None: run.output = update.output if update.error is not None: run.error = update.error if update.metadata is not None: run.metadata_ = update.metadata if update.finished_at is not None: run.finished_at = datetime.fromisoformat(update.finished_at) elif update.status in ("success", "failed"): run.finished_at = datetime.now(timezone.utc) db.commit() return {"id": run.id, "status": run.status} @app.get("/api/runs") def list_runs(limit: int = 50, user: str = Depends(require_auth), db: Session = Depends(get_db)): runs = db.query(Run).order_by(Run.started_at.desc()).limit(limit).all() return [{ "id": r.id, "agent_id": r.agent_id, "started_at": r.started_at.isoformat() if r.started_at else None, "finished_at": r.finished_at.isoformat() if r.finished_at else None, "status": r.status, "output": r.output, "error": r.error, "metadata": r.metadata_, } for r in runs] # --- Static files (frontend) --- app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/") def root(session: Optional[str] = Cookie(None)): user = get_current_user(session) if not user: return RedirectResponse("/login", status_code=302) return FileResponse("static/index.html") # --- Startup --- @app.on_event("startup") def startup(): init_db()