import logging import pathlib import re import subprocess from urllib.parse import urlsplit from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from app.config import settings def _database_runtime_status() -> dict: """Return safe-to-expose database mode metadata for diagnostics.""" resolved_url = settings.resolved_database_url status = { "mode": settings.db_mode, "url_scheme": urlsplit(resolved_url).scheme or "unknown", } if settings.db_mode == "local": status["local_db_path"] = str(settings.local_db_path) else: # Do not expose DATABASE_URL: it may contain credentials. status["database_url_set"] = bool(settings.database_url.strip()) return status def _resolve_git_sha() -> str: """Return the short git SHA of the backend directory, or 'unknown'.""" try: result = subprocess.run( ["git", "rev-parse", "--short", "HEAD"], cwd=pathlib.Path(__file__).resolve().parent.parent, capture_output=True, text=True, timeout=5, ) if result.returncode == 0: return result.stdout.strip() except Exception: pass return "unknown" class SecretGuardFilter(logging.Filter): def filter(self, record): if isinstance(record.msg, str) and re.search(r'sk-[a-zA-Z0-9_\-]+', record.msg): record.msg = re.sub(r'sk-[a-zA-Z0-9_\-]+', '[REDACTED_SECRET]', record.msg) return True def create_app() -> FastAPI: """Create and configure the FastAPI application.""" app = FastAPI( title=settings.app_name, description="Cross-agent memory handoff system", version="0.1.0", ) # CORS app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Secret Guard for handler in logging.root.handlers: handler.addFilter(SecretGuardFilter()) from app.api.routes import ( chat, checkpoint, claims, commits, context_git, current, lineage, metrics, repos, worktrees, ) # V2 Routes (Git for memory) app.include_router(repos.router, prefix="/api/v2", tags=["repos"]) app.include_router(commits.router, prefix="/api/v2", tags=["commits"]) app.include_router(context_git.router, prefix="/api/v2", tags=["commits"]) # V4 Routes (Chat workspace) app.include_router(chat.router, prefix="/api/v4", tags=["chat-v4"]) # V5 Routes app.include_router(checkpoint.router, prefix="/api/v5/checkpoint", tags=["checkpoint-v5"]) app.include_router(lineage.router, prefix="/api/v5", tags=["lineage-v5"]) app.include_router(claims.router, prefix="/api/v5", tags=["claims-v5"]) app.include_router(metrics.router, prefix="/api/v5", tags=["metrics-v5"]) app.include_router(current.router, prefix="/api/v5", tags=["current-v5"]) app.include_router(worktrees.router, prefix="/api/v5", tags=["worktrees-v5"]) # ── Capabilities manifest ──────────────────────────────────────── # Computed once at startup so /health is zero-cost at request time. _git_sha = _resolve_git_sha() # Hardcoded feature flags matching the route modules included above. # When a new feature ships (new route module, new query param, new # schema shape), add it here so agents can detect stale backends. _capabilities = [ "claims", # /api/v5/claims "structured_tasks", # task objects with intent_hint/blocked_by/status "task_ids", # structured task ids + claim.task_id support "checkpoint_notes", # /api/v5/checkpoint/{id}/notes "branch_disposition", # PATCH /api/v5/lineage/branches/disposition "freshness", # since_commit_id on state endpoint "compact_state", # --compact mode on state brief "worktrees", # /api/v5/worktrees "worktree_binding", # claims can bind to worktrees + state drift summary "activation_health", # /health includes DB mode + provider confidence ] @app.get("/health") async def health_check(): from app.config_loader import providers_status return { "status": "ok", "git_sha": _git_sha, "capabilities": _capabilities, "database": _database_runtime_status(), "providers": providers_status(), } @app.on_event("startup") async def startup_event(): import logging from app.config_loader import providers_status logger = logging.getLogger("smriti.startup") status = providers_status() logger.info("Provider runtime validation:") for name, info in status.items(): if info.get("missing_package", False): logger.warning(f" - {name}: disabled (package not installed)") elif not info.get("has_key", False): logger.info(f" - {name}: disabled (no API key configured)") elif not info.get("enabled", False): logger.info(f" - {name}: disabled (explicitly disabled in config)") else: logger.info(f" - {name}: configured and ready") # Specific check for the background provider — this is what # powers checkpoint extraction, drafting, and review. If it's # not configured, every LLM-backed endpoint will silently # return mock content, which is the single most confusing # failure mode in local development. bg = status.get("background_intelligence", {}) if not bg.get("configured"): bg_provider = bg.get("provider", "openai") logger.warning( "⚠ Background intelligence provider '%s' is NOT configured. " "Checkpoint extraction, drafting, and review will return mock " "content instead of real LLM output. To fix: set %s in .env " "or add the key to config/providers.yaml.", bg_provider, { "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "openrouter": "OPENROUTER_API_KEY", }.get(bg_provider, f"{bg_provider.upper()}_API_KEY"), ) return app app = create_app()