"""FastAPI application entry point."""
import asyncio
from contextlib import asynccontextmanager
from pathlib import Path
import logging
import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from navi.api.routes import agents, api_tokens, auth, health, messages, sessions, webhooks
from navi.api.routes.admin import router as admin_router
from navi.api.websocket import router as ws_router
from navi.config import settings
from navi.core.container import create_container
from debug.eval.api import router as eval_router
structlog.configure(
wrapper_class=structlog.make_filtering_bound_logger(
getattr(logging, settings.log_level)
),
)
# Suppress noisy MCP SDK health-check chatter.
logging.getLogger("mcp.server.lowlevel.server").setLevel(logging.WARNING)
_base = Path(__file__).parent.parent
@asynccontextmanager
async def lifespan(app: FastAPI):
log = structlog.get_logger()
container = await create_container()
app.state.container = container
from navi.api.deps import set_container
set_container(container)
if not settings.navi_auth_enabled:
log.warning(
"startup.auth_disabled",
message="Authorization is disabled — the server is open to anyone with network access. "
"Use NAVI_AUTH_ENABLED=false only for trusted single-user/local deployments.",
)
from navi.content_store import ensure_tables
from navi.session_files import cleanup_loop
from navi.auth import _ensure_auth_tables
from navi.profiles._overrides import ensure_table, load_overrides
from navi.api.routes.health import _check_embed
from navi.core.scheduler import recall_scheduler_loop
# Ensure auth tables first (navi_users is referenced by other DDL).
for attempt in range(1, 6):
try:
await _ensure_auth_tables()
await ensure_tables()
break
except Exception as e:
if attempt < 5:
log.warning("startup.ensure_tables_retry", attempt=attempt, error=str(e))
await asyncio.sleep(2)
else:
log.error("startup.ensure_tables_failed", error=str(e))
# Apply persisted profile overrides
try:
pool = await container.database.pool()
await ensure_table(pool)
overrides = await load_overrides(pool)
if overrides:
for pid, is_admin_only in overrides.items():
try:
profile = container.profile_registry.get(pid)
profile.is_admin_only = is_admin_only
except Exception:
pass
log.info("startup.profile_overrides_applied", count=len(overrides))
except Exception:
log.warning("startup.profile_overrides_failed", exc_info=True)
# Check embedding backend health
embed_status = await _check_embed()
if embed_status["ok"]:
log.info("startup.embed_ready", backend=embed_status["backend"])
else:
log.warning("startup.embed_unavailable", backend=embed_status["backend"], error=embed_status["error"])
# Start background tasks
cleanup_task = asyncio.create_task(cleanup_loop(container.session_store))
scheduler_task = asyncio.create_task(
recall_scheduler_loop(container.scheduler, container.session_store, container.orchestrator)
)
yield
# Shutdown
scheduler_task.cancel()
try:
await scheduler_task
except asyncio.CancelledError:
pass
cleanup_task.cancel()
try:
await cleanup_task
except asyncio.CancelledError:
pass
from navi.tools.ssh_exec import close_all_connections
close_all_connections()
await container.shutdown()
app = FastAPI(
title="Navi",
description="Modular agent system — REST API and WebSocket",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health.router)
app.include_router(auth.router)
app.include_router(api_tokens.router)
app.include_router(agents.router)
app.include_router(sessions.router)
app.include_router(messages.router)
app.include_router(ws_router)
app.include_router(webhooks.router)
app.include_router(admin_router)
app.include_router(eval_router)
app.mount("/assets", StaticFiles(directory=str(_base / "webclient" / "dist" / "assets")), name="assets")
app.mount("/images", StaticFiles(directory=str(_base / "webclient" / "dist" / "images")), name="images")
app.mount("/content-viewers", StaticFiles(directory=str(_base / "webclient" / "dist" / "content-viewers")), name="content_viewers")
app.mount("/content", StaticFiles(directory=str(_base / "navi" / "content")), name="content")
from fastapi.responses import RedirectResponse
@app.get("/api/sessions/{session_id}/files/{filename}", include_in_schema=False)
async def api_session_file_redirect(session_id: str, filename: str, download: bool = False):
target = f"/sessions/{session_id}/files/{filename}"
if download:
target += "?download=1"
return RedirectResponse(url=target, status_code=307)
@app.get("/", include_in_schema=False)
async def index() -> FileResponse:
return FileResponse(str(_base / "webclient" / "dist" / "index.html"), headers={"Cache-Control": "no-store"})
@app.get("/debug", include_in_schema=False)
async def debug() -> FileResponse:
return FileResponse("debug/index.html", headers={"Cache-Control": "no-store"})
@app.get("/debug/eval", include_in_schema=False)
async def debug_eval() -> FileResponse:
return FileResponse("debug/eval/index.html", headers={"Cache-Control": "no-store"})
@app.get("/admin", include_in_schema=False)
async def admin_panel() -> FileResponse:
return FileResponse("admin/index.html", headers={"Cache-Control": "no-store"})