2025-12-10 02:49:54 +01:00
|
|
|
"""
|
|
|
|
|
FastAPI Application Factory
|
|
|
|
|
|
|
|
|
|
Creates and configures the FastAPI application with all routes,
|
|
|
|
|
middleware, and event handlers.
|
|
|
|
|
"""
|
|
|
|
|
|
2026-07-09 01:59:23 +02:00
|
|
|
import asyncio
|
2025-12-10 02:49:54 +01:00
|
|
|
from collections.abc import AsyncGenerator
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
2026-07-03 19:24:00 +02:00
|
|
|
from roboco.api.auth.routes import mount_cloud_auth
|
|
|
|
|
from roboco.api.auth.seed import ensure_seed_user_startup
|
2026-06-29 05:38:21 +02:00
|
|
|
from roboco.api.deps import _auth_required, get_orchestrator_or_none
|
2025-12-10 17:35:43 +01:00
|
|
|
from roboco.api.middleware import setup_middleware
|
2025-12-28 19:52:01 +01:00
|
|
|
from roboco.api.routes.a2a import router as a2a_router
|
|
|
|
|
from roboco.api.routes.a2a import wellknown_router as a2a_wellknown_router
|
2025-12-18 00:32:54 +01:00
|
|
|
from roboco.api.routes.agents import router as agents_router
|
2026-07-25 05:39:44 +02:00
|
|
|
from roboco.api.routes.board_programs import router as board_programs_router
|
2026-06-15 20:47:41 +02:00
|
|
|
from roboco.api.routes.cockpit import router as cockpit_router
|
|
|
|
|
from roboco.api.routes.company_goals import router as company_goals_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.coroner import router as coroner_router
|
2025-12-12 02:45:47 +01:00
|
|
|
from roboco.api.routes.dashboard import router as dashboard_router
|
2026-01-04 03:03:45 +01:00
|
|
|
from roboco.api.routes.docs import router as docs_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.dogfood import router as dogfood_router
|
2025-12-30 18:55:32 +01:00
|
|
|
from roboco.api.routes.git import router as git_router
|
2026-07-21 00:54:20 +02:00
|
|
|
from roboco.api.routes.github_app import router as github_app_router
|
2025-12-12 02:45:47 +01:00
|
|
|
from roboco.api.routes.health import router as health_router
|
|
|
|
|
from roboco.api.routes.journals import router as journals_router
|
|
|
|
|
from roboco.api.routes.kanban import router as kanban_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.mirror import router as mirror_router
|
2025-12-12 02:45:47 +01:00
|
|
|
from roboco.api.routes.notifications import router as notifications_router
|
|
|
|
|
from roboco.api.routes.optimal import router as optimal_router
|
|
|
|
|
from roboco.api.routes.orchestrator import router as orchestrator_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.periscope import router as periscope_router
|
|
|
|
|
from roboco.api.routes.pest_control import router as pest_control_router
|
2026-06-15 20:47:41 +02:00
|
|
|
from roboco.api.routes.pitch import router as pitch_router
|
2026-06-26 01:43:08 +02:00
|
|
|
from roboco.api.routes.playbooks import router as playbooks_router
|
2026-06-03 06:35:03 +02:00
|
|
|
from roboco.api.routes.product import router as product_router
|
2025-12-30 18:55:32 +01:00
|
|
|
from roboco.api.routes.project import router as project_router
|
2026-06-09 17:08:34 +02:00
|
|
|
from roboco.api.routes.prompter_live import router as prompter_live_router
|
2026-05-01 05:41:10 +02:00
|
|
|
from roboco.api.routes.provider import router as provider_router
|
2026-06-26 01:43:08 +02:00
|
|
|
from roboco.api.routes.release import router as release_router
|
2026-06-15 20:47:41 +02:00
|
|
|
from roboco.api.routes.research import router as research_router
|
2026-07-03 19:24:00 +02:00
|
|
|
from roboco.api.routes.roadmap import router as roadmap_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.scales import router as scales_router
|
2026-06-15 20:47:41 +02:00
|
|
|
from roboco.api.routes.secretary import router as secretary_router
|
|
|
|
|
from roboco.api.routes.secretary_live import router as secretary_live_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.sentinel import router as sentinel_router
|
2026-06-12 23:11:01 +02:00
|
|
|
from roboco.api.routes.settings import router as settings_router
|
2026-07-25 17:13:32 +02:00
|
|
|
from roboco.api.routes.spackle import router as spackle_router
|
2025-12-12 02:45:47 +01:00
|
|
|
from roboco.api.routes.stream import router as stream_router
|
2026-06-11 09:21:26 +02:00
|
|
|
from roboco.api.routes.system import router as system_router
|
2025-12-12 02:45:47 +01:00
|
|
|
from roboco.api.routes.tasks import router as tasks_router
|
2026-07-18 02:47:59 +02:00
|
|
|
from roboco.api.routes.telegram import mount_telegram_miniapp_auth
|
2026-07-15 05:45:57 +02:00
|
|
|
from roboco.api.routes.telegram import router as telegram_router
|
2026-06-10 14:38:44 +02:00
|
|
|
from roboco.api.routes.usage import router as usage_router
|
2026-06-03 06:35:03 +02:00
|
|
|
from roboco.api.routes.v1 import do as do_module
|
|
|
|
|
from roboco.api.routes.v1 import flow_auditor as flow_auditor_module
|
|
|
|
|
from roboco.api.routes.v1 import flow_board as flow_board_module
|
|
|
|
|
from roboco.api.routes.v1 import flow_cell_pm as flow_cell_pm_module
|
|
|
|
|
from roboco.api.routes.v1 import flow_dev as flow_dev_module
|
|
|
|
|
from roboco.api.routes.v1 import flow_doc as flow_doc_module
|
|
|
|
|
from roboco.api.routes.v1 import flow_main_pm as flow_main_pm_module
|
2026-06-16 11:17:39 +02:00
|
|
|
from roboco.api.routes.v1 import flow_pr_reviewer as flow_pr_reviewer_module
|
2026-06-03 06:35:03 +02:00
|
|
|
from roboco.api.routes.v1 import flow_qa as flow_qa_module
|
2026-07-05 13:37:17 +02:00
|
|
|
from roboco.api.routes.video import router as video_router
|
|
|
|
|
from roboco.api.routes.video import tiktok_router
|
2025-12-30 18:55:32 +01:00
|
|
|
from roboco.api.routes.work_session import router as work_session_router
|
2026-07-03 19:24:00 +02:00
|
|
|
from roboco.api.routes.x import router as x_router
|
2025-12-12 02:45:47 +01:00
|
|
|
from roboco.api.websocket import router as ws_router
|
2025-12-10 02:49:54 +01:00
|
|
|
from roboco.config import settings
|
2026-06-17 16:35:43 +02:00
|
|
|
from roboco.db.base import close_db, get_session_factory, init_db
|
2025-12-10 17:35:43 +01:00
|
|
|
from roboco.logging import get_logger, setup_logging
|
2026-07-01 23:54:48 +02:00
|
|
|
from roboco.security import apply_guard, guarded_lifespan
|
2025-12-10 17:35:43 +01:00
|
|
|
from roboco.services.extraction import ExtractionPipeline, ExtractionService
|
2026-06-04 06:02:52 +02:00
|
|
|
from roboco.services.learning import get_learning_service
|
2025-12-10 17:35:43 +01:00
|
|
|
from roboco.services.optimal import close_optimal_service, get_optimal_service
|
2026-07-07 10:09:23 +02:00
|
|
|
from roboco.services.playbook import PlaybookService
|
2026-07-08 16:03:00 +02:00
|
|
|
from roboco.services.rag_index_failures import backfill_unindexed_journals, reclaim_due
|
2026-06-17 16:35:43 +02:00
|
|
|
from roboco.services.settings import apply_persisted_feature_flags
|
2025-12-10 02:49:54 +01:00
|
|
|
from roboco.services.transcription import TranscriptionService
|
|
|
|
|
|
|
|
|
|
# Setup logging before anything else
|
|
|
|
|
setup_logging()
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
2025-12-12 02:45:47 +01:00
|
|
|
|
|
|
|
|
class _AppServices:
|
|
|
|
|
"""Holder for application service instances (initialized in lifespan)."""
|
|
|
|
|
|
|
|
|
|
transcription: TranscriptionService | None = None
|
|
|
|
|
extraction: ExtractionPipeline | None = None
|
2025-12-10 02:49:54 +01:00
|
|
|
|
|
|
|
|
|
2026-07-07 10:09:23 +02:00
|
|
|
async def _reconcile_unindexed_playbooks(app: FastAPI) -> None:
|
|
|
|
|
"""Re-index APPROVED playbooks left ``indexed_ok=False`` by a failed
|
|
|
|
|
post-commit embed (e.g. an Ollama restart mid-approval-burst). Best-effort:
|
|
|
|
|
a failure here never blocks startup — rows stay unindexed and the next
|
|
|
|
|
startup retries them. Skipped when RAG is disabled (no optimal) or the
|
|
|
|
|
org-memory loop is off (the index is inert).
|
|
|
|
|
"""
|
|
|
|
|
if app.state.optimal is None or not settings.org_memory_enabled:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
async with get_session_factory()() as session:
|
|
|
|
|
svc = PlaybookService(session)
|
|
|
|
|
reconciled = await svc.reconcile_unindexed_approved()
|
|
|
|
|
if reconciled:
|
|
|
|
|
logger.info(
|
|
|
|
|
"Playbook reconcile: re-indexed unindexed approved",
|
|
|
|
|
count=reconciled,
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Playbook reconcile failed; continuing", error=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _reclaim_rag_index_failures(app: FastAPI) -> None:
|
|
|
|
|
"""Reclaim dead-lettered RAG index writes (embedder 429 after retries, etc.).
|
|
|
|
|
|
|
|
|
|
Best-effort: a failure here never blocks startup — due rows stay in the
|
|
|
|
|
dead-letter and the next startup retries them. Skipped when RAG is
|
|
|
|
|
disabled (no optimal).
|
|
|
|
|
"""
|
|
|
|
|
if app.state.optimal is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
reclaimed = await reclaim_due(app.state.optimal)
|
|
|
|
|
if reclaimed:
|
|
|
|
|
logger.info(
|
|
|
|
|
"RAG index dead-letter reclaim: re-indexed rows",
|
|
|
|
|
count=reclaimed,
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("RAG index dead-letter reclaim failed; continuing", error=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-07-08 16:03:00 +02:00
|
|
|
async def _backfill_unindexed_journals(app: FastAPI) -> None:
|
|
|
|
|
"""Re-index journal/learning entries silently zero-chunked before the
|
|
|
|
|
per-index chunk-floor fix (see ``backfill_unindexed_journals``'s
|
|
|
|
|
docstring). Best-effort: a failure here never blocks startup — the rows
|
|
|
|
|
stay and the next startup retries them. Skipped when RAG is disabled.
|
|
|
|
|
"""
|
|
|
|
|
if app.state.optimal is None:
|
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
await backfill_unindexed_journals(app.state.optimal)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Journal/learning RAG backfill failed; continuing", error=str(e))
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 10:09:23 +02:00
|
|
|
async def _reconcile_rag_indexes(app: FastAPI) -> None:
|
2026-07-08 16:03:00 +02:00
|
|
|
"""Run all RAG index reconcile passes: playbooks, dead-letter reclaim,
|
|
|
|
|
and the journals/learnings zero-chunk backfill."""
|
2026-07-07 10:09:23 +02:00
|
|
|
await _reconcile_unindexed_playbooks(app)
|
|
|
|
|
await _reclaim_rag_index_failures(app)
|
2026-07-08 16:03:00 +02:00
|
|
|
await _backfill_unindexed_journals(app)
|
2026-07-09 01:59:23 +02:00
|
|
|
logger.info("RAG index reconcile finished")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _log_reconcile_outcome(task: asyncio.Task[None]) -> None:
|
|
|
|
|
"""Surface a background-reconcile crash; each pass already swallows its
|
|
|
|
|
own errors, so anything landing here is an unexpected bug, not a retry."""
|
|
|
|
|
if task.cancelled():
|
|
|
|
|
return
|
|
|
|
|
exc = task.exception()
|
|
|
|
|
if exc is not None:
|
|
|
|
|
logger.error("Background RAG reconcile crashed", error=repr(exc))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _schedule_rag_reconcile(app: FastAPI) -> asyncio.Task[None]:
|
|
|
|
|
"""Schedule the reconcile without awaiting it (see lifespan comment)."""
|
|
|
|
|
task = asyncio.create_task(_reconcile_rag_indexes(app))
|
|
|
|
|
task.add_done_callback(_log_reconcile_outcome)
|
|
|
|
|
app.state.rag_reconcile_task = task
|
|
|
|
|
return task
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cancel_rag_reconcile(app: FastAPI) -> None:
|
|
|
|
|
"""Stop a still-running background reconcile at shutdown."""
|
|
|
|
|
task = getattr(app.state, "rag_reconcile_task", None)
|
|
|
|
|
if task is not None and not task.done():
|
|
|
|
|
task.cancel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _apply_flag_overrides() -> None:
|
|
|
|
|
"""Overlay panel-persisted feature-flag overrides onto the live config so
|
|
|
|
|
the rest of startup (and the dispatch loops) read the panel's choices;
|
|
|
|
|
unset flags keep their env/config default. Best-effort — a failure here
|
|
|
|
|
must not block startup, the env defaults still apply."""
|
|
|
|
|
try:
|
|
|
|
|
async with get_session_factory()() as flags_db:
|
|
|
|
|
applied_flags = await apply_persisted_feature_flags(flags_db)
|
|
|
|
|
if applied_flags:
|
|
|
|
|
logger.info("Applied persisted feature-flag overrides", flags=applied_flags)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Feature-flag overlay failed; using env defaults", error=str(e))
|
2026-07-07 10:09:23 +02:00
|
|
|
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
@asynccontextmanager
|
2025-12-10 17:35:43 +01:00
|
|
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
2025-12-10 02:49:54 +01:00
|
|
|
"""
|
|
|
|
|
Application lifespan manager.
|
|
|
|
|
|
|
|
|
|
Handles startup and shutdown events.
|
|
|
|
|
"""
|
|
|
|
|
logger.info(
|
|
|
|
|
"Starting RoboCo API",
|
|
|
|
|
version=settings.app_version,
|
|
|
|
|
environment=settings.environment,
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-05 16:35:22 +02:00
|
|
|
if not _auth_required():
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Agent auth is in HEADER-TRUST mode (ROBOCO_AGENT_AUTH_REQUIRED is "
|
|
|
|
|
"not set to true): the API accepts X-Agent-Id / X-Agent-Role without "
|
|
|
|
|
"verifying a signed token, so any client that can reach it may act as "
|
|
|
|
|
"any role, including 'ceo'. Acceptable only on a trusted private "
|
|
|
|
|
"network. Set ROBOCO_AGENT_AUTH_REQUIRED=true and do NOT expose this "
|
|
|
|
|
"API to untrusted networks.",
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-20 15:10:54 +02:00
|
|
|
# Startup: apply Alembic migrations (+ create_all fallback for fresh DBs).
|
|
|
|
|
# init_db runs on every environment now — migrations are idempotent via
|
|
|
|
|
# alembic_version, and this is the only way new schema (e.g. enum value
|
|
|
|
|
# additions like NotificationType.APPROVAL) reaches the running DB.
|
|
|
|
|
await init_db()
|
|
|
|
|
logger.info("Database initialized")
|
2025-12-10 02:49:54 +01:00
|
|
|
|
2026-07-03 19:24:00 +02:00
|
|
|
# Cloud auth: idempotently upsert the single seeded CEO login user.
|
|
|
|
|
# No-op unless ROBOCO_CLOUD_AUTH_ENABLED (see ensure_seed_user_startup).
|
|
|
|
|
await ensure_seed_user_startup()
|
|
|
|
|
|
2026-07-09 01:59:23 +02:00
|
|
|
await _apply_flag_overrides()
|
2026-06-17 16:35:43 +02:00
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
# Initialize Phase 2 services
|
2025-12-12 02:45:47 +01:00
|
|
|
_AppServices.transcription = TranscriptionService()
|
|
|
|
|
await _AppServices.transcription.start()
|
2025-12-10 02:49:54 +01:00
|
|
|
|
|
|
|
|
extraction_service = ExtractionService()
|
2025-12-12 02:45:47 +01:00
|
|
|
_AppServices.extraction = ExtractionPipeline(extraction_service)
|
2025-12-10 02:49:54 +01:00
|
|
|
|
|
|
|
|
# Store in app state for access in routes
|
2025-12-12 02:45:47 +01:00
|
|
|
app.state.transcription = _AppServices.transcription
|
|
|
|
|
app.state.extraction = _AppServices.extraction
|
2025-12-10 02:49:54 +01:00
|
|
|
|
2026-01-01 22:15:23 +01:00
|
|
|
# Initialize OptimalService (RAG) - BLOCKS until fully ready
|
|
|
|
|
# This ensures /health only returns 200 when RAG is operational
|
|
|
|
|
# Typical initialization time: 30-90 seconds (embedding + indexing)
|
2025-12-15 02:13:20 +01:00
|
|
|
try:
|
2026-01-01 22:15:23 +01:00
|
|
|
logger.info("Initializing OptimalService (RAG)...")
|
2025-12-15 02:13:20 +01:00
|
|
|
optimal_service = await get_optimal_service()
|
|
|
|
|
app.state.optimal = optimal_service
|
|
|
|
|
logger.info("OptimalService (RAG) initialized successfully")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(
|
|
|
|
|
"OptimalService (RAG) initialization failed - RAG features disabled",
|
|
|
|
|
error=str(e),
|
|
|
|
|
)
|
|
|
|
|
app.state.optimal = None
|
2025-12-10 02:49:54 +01:00
|
|
|
|
2026-06-04 06:02:52 +02:00
|
|
|
# Wire the learning-propagation singleton to OptimalService. Without this,
|
|
|
|
|
# record_learning() raises "not initialized" and every task completion logs
|
|
|
|
|
# "Failed to extract learnings". Skipped when RAG is disabled (no optimal).
|
|
|
|
|
if app.state.optimal is not None:
|
|
|
|
|
try:
|
|
|
|
|
learning_service = await get_learning_service()
|
|
|
|
|
await learning_service.initialize(app.state.optimal)
|
|
|
|
|
logger.info("LearningPropagationService initialized")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("LearningPropagationService init failed", error=str(e))
|
|
|
|
|
|
2026-07-09 01:59:23 +02:00
|
|
|
# Reconcile RAG index state in the BACKGROUND: re-index APPROVED playbooks
|
|
|
|
|
# left unindexed by a failed post-commit embed, reclaim dead-lettered
|
|
|
|
|
# index writes, and backfill zero-chunk journals/learnings. Never awaited
|
|
|
|
|
# here — uvicorn binds the socket only after this lifespan completes, and
|
|
|
|
|
# a 200-entry backfill behind a busy Ollama held the API down for 30+
|
|
|
|
|
# minutes when this was a blocking await (2026-07-09 deploy).
|
|
|
|
|
_schedule_rag_reconcile(app)
|
2026-07-07 10:09:23 +02:00
|
|
|
|
2026-01-01 22:15:23 +01:00
|
|
|
logger.info("All services initialized, API ready")
|
2025-12-10 02:49:54 +01:00
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
# Shutdown
|
|
|
|
|
logger.info("Shutting down RoboCo API")
|
|
|
|
|
|
2026-07-09 01:59:23 +02:00
|
|
|
_cancel_rag_reconcile(app)
|
|
|
|
|
|
2025-12-12 02:45:47 +01:00
|
|
|
if _AppServices.transcription:
|
|
|
|
|
await _AppServices.transcription.stop()
|
2025-12-10 02:49:54 +01:00
|
|
|
|
2026-06-29 05:38:21 +02:00
|
|
|
# Stop the orchestrator BEFORE closing the DB / OptimalService. stop()
|
|
|
|
|
# cancels the background loops, stops the agents (finalizing work sessions
|
|
|
|
|
# + agent state via DB writes), and drains fire-and-forget bg writes
|
|
|
|
|
# (respawn_tracker upserts, audit-log rows) — all needing the DB still
|
|
|
|
|
# open. Closing the DB first silently dropped those final writes (the old
|
|
|
|
|
# order, where only bootstrap's finally block called stop() AFTER lifespan
|
|
|
|
|
# had already closed the DB). Best-effort: a stop error must not block the
|
|
|
|
|
# resource teardown below. No-op when no orchestrator is wired (tests,
|
|
|
|
|
# skip_orchestrator). bootstrap's finally block re-calls stop() as a safety
|
|
|
|
|
# net; stop() is idempotent (guarded by the _stopped flag) so the second
|
|
|
|
|
# call is a no-op.
|
|
|
|
|
orchestrator = get_orchestrator_or_none()
|
|
|
|
|
if orchestrator is not None:
|
|
|
|
|
try:
|
|
|
|
|
await orchestrator.stop()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Orchestrator stop failed during shutdown", error=str(e))
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
# Close Phase 3 services
|
|
|
|
|
await close_optimal_service()
|
|
|
|
|
|
|
|
|
|
await close_db()
|
|
|
|
|
|
|
|
|
|
logger.info("Shutdown complete")
|
|
|
|
|
|
|
|
|
|
|
2026-07-03 19:24:00 +02:00
|
|
|
def _mount_v1_routers(app: FastAPI) -> None:
|
|
|
|
|
"""Mount every API v1 (intent-verb + content-tool) router."""
|
|
|
|
|
app.include_router(flow_dev_module.router)
|
|
|
|
|
app.include_router(flow_qa_module.router)
|
|
|
|
|
app.include_router(flow_doc_module.router)
|
|
|
|
|
app.include_router(flow_cell_pm_module.router)
|
|
|
|
|
app.include_router(flow_main_pm_module.router)
|
|
|
|
|
app.include_router(flow_board_module.router)
|
|
|
|
|
app.include_router(flow_auditor_module.router)
|
|
|
|
|
app.include_router(flow_pr_reviewer_module.router)
|
|
|
|
|
app.include_router(do_module.router)
|
|
|
|
|
|
|
|
|
|
|
2026-07-25 17:13:32 +02:00
|
|
|
def _mount_board_program_routers(app: FastAPI, api_prefix: str) -> None:
|
|
|
|
|
"""Mount every Board Program route — the generic registry route plus each
|
|
|
|
|
program's own per-item / read-only surface. Grouped into one helper
|
|
|
|
|
(mirrors ``_mount_v1_routers``) to keep ``create_app``'s own statement
|
|
|
|
|
count from growing unbounded as programs are added."""
|
|
|
|
|
# Board roadmap engine — the CEO approves/rejects items within a held
|
|
|
|
|
# roadmap cycle. Approving materializes a BACKLOG task; nothing auto-starts.
|
|
|
|
|
app.include_router(roadmap_router, prefix=f"{api_prefix}/roadmap", tags=["Roadmap"])
|
|
|
|
|
# Board Programs — the generic registry status + off-schedule "run now"
|
|
|
|
|
# (roadmap + x_feature today; every later program rides the same route).
|
|
|
|
|
app.include_router(
|
|
|
|
|
board_programs_router,
|
|
|
|
|
prefix=f"{api_prefix}/board-programs",
|
|
|
|
|
tags=["Board Programs"],
|
|
|
|
|
)
|
|
|
|
|
# Pest Control (Board Program) — the CEO approves/rejects items within a
|
|
|
|
|
# held bug-hunt cycle. Approving materializes a BACKLOG task; nothing
|
|
|
|
|
# auto-starts.
|
|
|
|
|
app.include_router(
|
|
|
|
|
pest_control_router,
|
|
|
|
|
prefix=f"{api_prefix}/pest-control",
|
|
|
|
|
tags=["Pest Control"],
|
|
|
|
|
)
|
|
|
|
|
# Periscope (Board Program) — the CEO reads filed market-research briefs.
|
|
|
|
|
# Read-only: a brief is a report, not a queue item; nothing to approve.
|
|
|
|
|
app.include_router(
|
|
|
|
|
periscope_router, prefix=f"{api_prefix}/periscope", tags=["Periscope"]
|
|
|
|
|
)
|
|
|
|
|
# Coroner (Board Program) — read-only Postmortems list. A postmortem
|
|
|
|
|
# completes atomically at propose_postmortem time; there is nothing here
|
|
|
|
|
# for the CEO to approve/reject.
|
|
|
|
|
app.include_router(coroner_router, prefix=f"{api_prefix}/coroner", tags=["Coroner"])
|
|
|
|
|
# Sentinel (Board Program) — the CEO reads filed org-wide quality-drift
|
|
|
|
|
# reports. Read-only: a report is a report, not a queue item; nothing to
|
|
|
|
|
# approve.
|
|
|
|
|
app.include_router(
|
|
|
|
|
sentinel_router, prefix=f"{api_prefix}/sentinel", tags=["Sentinel"]
|
|
|
|
|
)
|
|
|
|
|
# Spackle (Board Program) — the CEO approves/rejects items within a held
|
|
|
|
|
# gap-fill cycle. Approving materializes a BACKLOG task; nothing
|
|
|
|
|
# auto-starts.
|
|
|
|
|
app.include_router(spackle_router, prefix=f"{api_prefix}/spackle", tags=["Spackle"])
|
|
|
|
|
# Scales (Board Program) — the CEO approves/rejects items within a held
|
|
|
|
|
# portfolio-rebalance cycle. Approving EXECUTES the item against the live
|
|
|
|
|
# target task (reprioritize or cancel); nothing here creates a task.
|
|
|
|
|
app.include_router(scales_router, prefix=f"{api_prefix}/scales", tags=["Scales"])
|
|
|
|
|
# Mirror (Board Program) — the CEO approves/rejects items within a held
|
|
|
|
|
# messaging-fixes cycle. Approving materializes a BACKLOG docs task;
|
|
|
|
|
# nothing auto-starts.
|
|
|
|
|
app.include_router(mirror_router, prefix=f"{api_prefix}/mirror", tags=["Mirror"])
|
|
|
|
|
# Dogfood (Board Program) — the CEO approves/rejects items within a held
|
|
|
|
|
# friction-fix cycle. Approving materializes a BACKLOG task; nothing
|
|
|
|
|
# auto-starts.
|
|
|
|
|
app.include_router(dogfood_router, prefix=f"{api_prefix}/dogfood", tags=["Dogfood"])
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
def create_app() -> FastAPI:
|
|
|
|
|
"""
|
|
|
|
|
Create and configure the FastAPI application.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Configured FastAPI application instance.
|
|
|
|
|
"""
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
title="RoboCo API",
|
|
|
|
|
description="AI Agents Company - Messaging and Task Management API",
|
|
|
|
|
version=settings.app_version,
|
2025-12-18 03:29:08 +01:00
|
|
|
docs_url="/docs", # if settings.debug else None,
|
|
|
|
|
redoc_url="/redoc", # if settings.debug else None,
|
2026-07-01 23:54:48 +02:00
|
|
|
# Wraps the existing lifespan with fastapi-guard's when armed (drives the
|
|
|
|
|
# middleware's redis/geo/agent init); returns it unchanged when off.
|
|
|
|
|
lifespan=guarded_lifespan(lifespan),
|
2025-12-10 02:49:54 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ==========================================================================
|
|
|
|
|
# Middleware
|
|
|
|
|
# ==========================================================================
|
|
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=settings.cors_origins,
|
|
|
|
|
allow_credentials=settings.cors_allow_credentials,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Setup custom middleware (error handling, logging, correlation IDs)
|
|
|
|
|
setup_middleware(app)
|
|
|
|
|
|
2026-07-01 23:54:48 +02:00
|
|
|
# fastapi-guard HTTP security layer — no-op unless ROBOCO_GUARD_ENABLED.
|
|
|
|
|
# Mounted last so SecurityMiddleware is outermost and blocks hostile traffic
|
|
|
|
|
# before it reaches the app (guard does its own request logging); order can
|
|
|
|
|
# be tuned during passive-mode calibration.
|
|
|
|
|
apply_guard(app)
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
# ==========================================================================
|
|
|
|
|
# Routes
|
|
|
|
|
# ==========================================================================
|
|
|
|
|
|
|
|
|
|
# Health check
|
2025-12-12 02:45:47 +01:00
|
|
|
app.include_router(health_router, tags=["Health"])
|
2025-12-10 02:49:54 +01:00
|
|
|
|
2025-12-28 19:52:01 +01:00
|
|
|
# A2A Protocol: Well-known endpoints at root level
|
|
|
|
|
# (/.well-known/agent.json, /agents/{id}/.well-known/agent.json)
|
|
|
|
|
app.include_router(a2a_wellknown_router, tags=["A2A Protocol"])
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
# API v1
|
2026-05-02 03:18:47 +02:00
|
|
|
api_prefix = "/api"
|
2025-12-10 02:49:54 +01:00
|
|
|
|
2025-12-18 00:32:54 +01:00
|
|
|
app.include_router(
|
|
|
|
|
agents_router,
|
|
|
|
|
prefix=f"{api_prefix}/agents",
|
|
|
|
|
tags=["Agents"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-12 23:11:01 +02:00
|
|
|
app.include_router(
|
|
|
|
|
settings_router,
|
|
|
|
|
prefix=f"{api_prefix}/settings",
|
|
|
|
|
tags=["Settings"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-15 20:47:41 +02:00
|
|
|
app.include_router(
|
|
|
|
|
company_goals_router,
|
|
|
|
|
prefix=f"{api_prefix}/company-goals",
|
|
|
|
|
tags=["Company"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
notifications_router,
|
2025-12-10 02:49:54 +01:00
|
|
|
prefix=f"{api_prefix}/notifications",
|
|
|
|
|
tags=["Notifications"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Phase 2: Stream processing and permissions
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
stream_router,
|
2025-12-10 02:49:54 +01:00
|
|
|
prefix=f"{api_prefix}/stream",
|
|
|
|
|
tags=["Stream Processing"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Phase 3: Intelligence - Optimal API and Journal API
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
optimal_router,
|
2025-12-15 20:59:31 +01:00
|
|
|
prefix=f"{api_prefix}/optimal",
|
2025-12-10 02:49:54 +01:00
|
|
|
tags=["Optimal API"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
journals_router,
|
2025-12-15 20:59:31 +01:00
|
|
|
prefix=f"{api_prefix}/journals",
|
2025-12-10 02:49:54 +01:00
|
|
|
tags=["Journals"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-15 20:47:41 +02:00
|
|
|
# Web research — pluggable external search/fetch for Board + PM agents.
|
|
|
|
|
app.include_router(
|
|
|
|
|
research_router,
|
|
|
|
|
prefix=f"{api_prefix}/research",
|
|
|
|
|
tags=["Research"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Cockpit — the CEO's read-only "is the business winning?" summary.
|
|
|
|
|
app.include_router(
|
|
|
|
|
cockpit_router,
|
|
|
|
|
prefix=f"{api_prefix}/cockpit",
|
|
|
|
|
tags=["Cockpit"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-26 01:43:08 +02:00
|
|
|
# Release manager — the CEO approves/rejects a held release proposal.
|
|
|
|
|
app.include_router(
|
|
|
|
|
release_router,
|
|
|
|
|
prefix=f"{api_prefix}/release",
|
|
|
|
|
tags=["Release"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Playbooks — the Auditor (or CEO) curates the drafted playbook library.
|
|
|
|
|
app.include_router(
|
|
|
|
|
playbooks_router,
|
|
|
|
|
prefix=f"{api_prefix}/playbooks",
|
|
|
|
|
tags=["Playbooks"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-03 19:24:00 +02:00
|
|
|
# X (Twitter) engine — the CEO approves/rejects held posts/replies and
|
|
|
|
|
# manages credentials. Nothing here posts except an explicit approve.
|
|
|
|
|
app.include_router(
|
|
|
|
|
x_router,
|
|
|
|
|
prefix=f"{api_prefix}/x",
|
|
|
|
|
tags=["X"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-25 17:13:32 +02:00
|
|
|
# Board Programs — the generic registry route + each program's own
|
|
|
|
|
# per-item / read-only surface (roadmap, board-programs, pest control,
|
|
|
|
|
# periscope, coroner, sentinel, spackle, scales). Grouped into one helper
|
|
|
|
|
# to keep this function's own statement count from growing unbounded as
|
|
|
|
|
# programs are added (mirrors ``_mount_v1_routers`` below).
|
|
|
|
|
_mount_board_program_routers(app, api_prefix)
|
2026-07-25 05:39:44 +02:00
|
|
|
|
2026-07-05 13:37:17 +02:00
|
|
|
# Video engine — the CEO requests an on-demand marketing video; the
|
|
|
|
|
# release/spotlight triggers open the same UX/UI authoring task via their
|
|
|
|
|
# own hooks. Nothing renders or posts from this route alone.
|
|
|
|
|
app.include_router(
|
|
|
|
|
video_router,
|
|
|
|
|
prefix=f"{api_prefix}/video",
|
|
|
|
|
tags=["Video"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# TikTok credentials — write-only OAuth2 secrets for the video engine's
|
|
|
|
|
# inbox-upload poster (mirrors /x/credentials).
|
|
|
|
|
app.include_router(
|
|
|
|
|
tiktok_router,
|
|
|
|
|
prefix=f"{api_prefix}/tiktok",
|
|
|
|
|
tags=["TikTok"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-15 05:45:57 +02:00
|
|
|
# Telegram notifications bridge — CEO-managed bot-token + chat-id credentials
|
|
|
|
|
# (write-only); the fan-out itself runs server-side from the CEO producers.
|
|
|
|
|
app.include_router(
|
|
|
|
|
telegram_router,
|
|
|
|
|
prefix=f"{api_prefix}/telegram",
|
|
|
|
|
tags=["Telegram"],
|
|
|
|
|
)
|
2026-07-18 02:47:59 +02:00
|
|
|
# Telegram Mini App sign-in — public, pre-auth; mounted only when both
|
|
|
|
|
# telegram_miniapp_enabled and cloud_auth_enabled are armed.
|
|
|
|
|
mount_telegram_miniapp_auth(app, f"{api_prefix}/telegram")
|
2026-07-15 05:45:57 +02:00
|
|
|
|
2026-06-15 20:47:41 +02:00
|
|
|
# Pitches — Board proposals + CEO approve -> auto-provision origination path.
|
|
|
|
|
app.include_router(
|
|
|
|
|
pitch_router,
|
|
|
|
|
prefix=f"{api_prefix}/pitches",
|
|
|
|
|
tags=["Pitches"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Secretary — the CEO's chief-of-staff: company-state reads + gated directives.
|
|
|
|
|
app.include_router(
|
|
|
|
|
secretary_router,
|
|
|
|
|
prefix=f"{api_prefix}/secretary",
|
|
|
|
|
tags=["Secretary"],
|
|
|
|
|
)
|
|
|
|
|
# Secretary live chat — panel <-> Secretary container bridge.
|
|
|
|
|
app.include_router(
|
|
|
|
|
secretary_live_router,
|
|
|
|
|
prefix=f"{api_prefix}/secretary",
|
|
|
|
|
tags=["Secretary"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
# Phase 5: Management - Tasks, Kanban, Dashboards
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
tasks_router,
|
2025-12-15 20:59:31 +01:00
|
|
|
prefix=f"{api_prefix}/tasks",
|
2025-12-10 02:49:54 +01:00
|
|
|
tags=["Tasks"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
kanban_router,
|
2025-12-15 20:59:31 +01:00
|
|
|
prefix=f"{api_prefix}/kanban",
|
2025-12-10 02:49:54 +01:00
|
|
|
tags=["Kanban"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
dashboard_router,
|
2025-12-15 20:59:31 +01:00
|
|
|
prefix=f"{api_prefix}/dashboard",
|
2025-12-10 02:49:54 +01:00
|
|
|
tags=["Dashboard"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-10 17:35:43 +01:00
|
|
|
# Phase 7: Agent Runtime
|
|
|
|
|
app.include_router(
|
2025-12-12 02:45:47 +01:00
|
|
|
orchestrator_router,
|
2025-12-10 17:35:43 +01:00
|
|
|
prefix=f"{api_prefix}/orchestrator",
|
|
|
|
|
tags=["Orchestrator"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-28 19:52:01 +01:00
|
|
|
# A2A Protocol: API endpoints
|
|
|
|
|
app.include_router(
|
|
|
|
|
a2a_router,
|
|
|
|
|
prefix=f"{api_prefix}/a2a",
|
|
|
|
|
tags=["A2A Protocol"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-30 18:55:32 +01:00
|
|
|
# Git Integration
|
|
|
|
|
app.include_router(
|
|
|
|
|
git_router,
|
|
|
|
|
prefix=f"{api_prefix}/git",
|
|
|
|
|
tags=["Git Operations"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-21 00:54:20 +02:00
|
|
|
# GitHub App integration — CEO-managed credentials + the "Select repo"
|
|
|
|
|
# picker's installation/repository listing (see roboco/services/github_app_auth.py).
|
|
|
|
|
app.include_router(
|
|
|
|
|
github_app_router,
|
|
|
|
|
prefix=f"{api_prefix}/github-app",
|
|
|
|
|
tags=["GitHub App"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-30 18:55:32 +01:00
|
|
|
# Project Management
|
|
|
|
|
app.include_router(
|
|
|
|
|
project_router,
|
|
|
|
|
prefix=f"{api_prefix}/projects",
|
|
|
|
|
tags=["Projects"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-03 06:35:03 +02:00
|
|
|
# Product Management
|
|
|
|
|
app.include_router(
|
|
|
|
|
product_router,
|
|
|
|
|
prefix=f"{api_prefix}/products",
|
|
|
|
|
tags=["Products"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-01 05:41:10 +02:00
|
|
|
# AI Providers (model routing + Ollama-cloud fallback)
|
|
|
|
|
app.include_router(
|
|
|
|
|
provider_router,
|
|
|
|
|
prefix=f"{api_prefix}/providers",
|
|
|
|
|
tags=["Providers"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-09 17:08:34 +02:00
|
|
|
# Prompter live chat — panel <-> spawned intake agent (SSE + relay)
|
|
|
|
|
app.include_router(
|
|
|
|
|
prompter_live_router,
|
|
|
|
|
prefix=f"{api_prefix}/prompter",
|
|
|
|
|
tags=["Prompter"],
|
|
|
|
|
)
|
2026-06-07 22:16:56 +02:00
|
|
|
|
2025-12-30 18:55:32 +01:00
|
|
|
# Work Sessions
|
|
|
|
|
app.include_router(
|
|
|
|
|
work_session_router,
|
|
|
|
|
prefix=f"{api_prefix}/work-sessions",
|
|
|
|
|
tags=["Work Sessions"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-04 03:03:45 +01:00
|
|
|
# Documentation
|
|
|
|
|
app.include_router(
|
|
|
|
|
docs_router,
|
|
|
|
|
prefix=f"{api_prefix}/docs",
|
|
|
|
|
tags=["Documentation"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-10 14:38:44 +02:00
|
|
|
# Token Usage Analytics
|
|
|
|
|
app.include_router(
|
|
|
|
|
usage_router,
|
|
|
|
|
prefix=f"{api_prefix}/usage",
|
|
|
|
|
tags=["Usage Analytics"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-11 09:21:26 +02:00
|
|
|
# System monitoring (rate-limits, etc.)
|
|
|
|
|
app.include_router(
|
|
|
|
|
system_router,
|
|
|
|
|
prefix=f"{api_prefix}/system",
|
|
|
|
|
tags=["System"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-03 19:24:00 +02:00
|
|
|
# Cloud auth — /auth/status is always public; login/logout mount only
|
|
|
|
|
# when ROBOCO_CLOUD_AUTH_ENABLED (mirrors apply_guard's conditional mount).
|
|
|
|
|
mount_cloud_auth(app, f"{api_prefix}/auth")
|
2026-05-02 03:11:49 +02:00
|
|
|
|
2026-07-03 19:24:00 +02:00
|
|
|
# API v1 — intent-verb flow + content-tool endpoints (each module owns
|
|
|
|
|
# its own prefix); grouped into one helper to keep create_app's own
|
|
|
|
|
# statement count from growing unbounded as roles are added.
|
|
|
|
|
_mount_v1_routers(app)
|
2026-05-02 03:11:49 +02:00
|
|
|
|
2025-12-10 02:49:54 +01:00
|
|
|
# ==========================================================================
|
|
|
|
|
# WebSocket
|
|
|
|
|
# ==========================================================================
|
|
|
|
|
app.include_router(ws_router, prefix="/ws", tags=["WebSocket"])
|
|
|
|
|
|
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Create the default application instance
|
|
|
|
|
app = create_app()
|