2026-02-09 15:49:50 +05:30
|
|
|
"""FastAPI application entrypoint and router wiring for the backend."""
|
|
|
|
|
|
2026-02-01 22:25:28 +05:30
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-02-06 11:57:29 +05:30
|
|
|
from contextlib import asynccontextmanager
|
2026-02-15 02:57:06 +05:30
|
|
|
from typing import TYPE_CHECKING, Any
|
2026-02-06 11:57:29 +05:30
|
|
|
|
2026-02-24 17:36:44 +01:00
|
|
|
from fastapi import APIRouter, FastAPI, status
|
2026-02-01 22:25:28 +05:30
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2026-02-15 03:02:54 +05:30
|
|
|
from fastapi.openapi.utils import get_openapi
|
2026-02-06 19:11:11 +05:30
|
|
|
from fastapi_pagination import add_pagination
|
2026-02-01 22:25:28 +05:30
|
|
|
|
2026-02-04 02:28:51 +05:30
|
|
|
from app.api.activity import router as activity_router
|
2026-02-05 19:06:32 +05:30
|
|
|
from app.api.agent import router as agent_router
|
2026-02-04 02:28:51 +05:30
|
|
|
from app.api.agents import router as agents_router
|
2026-02-05 14:43:25 +05:30
|
|
|
from app.api.approvals import router as approvals_router
|
2026-02-04 02:28:51 +05:30
|
|
|
from app.api.auth import router as auth_router
|
2026-02-07 20:29:50 +05:30
|
|
|
from app.api.board_group_memory import router as board_group_memory_router
|
|
|
|
|
from app.api.board_groups import router as board_groups_router
|
2026-02-05 14:43:25 +05:30
|
|
|
from app.api.board_memory import router as board_memory_router
|
|
|
|
|
from app.api.board_onboarding import router as board_onboarding_router
|
2026-02-13 00:31:32 +05:30
|
|
|
from app.api.board_webhooks import router as board_webhooks_router
|
2026-02-04 02:28:51 +05:30
|
|
|
from app.api.boards import router as boards_router
|
|
|
|
|
from app.api.gateway import router as gateway_router
|
2026-02-04 23:07:22 +05:30
|
|
|
from app.api.gateways import router as gateways_router
|
2026-02-04 20:49:25 +05:30
|
|
|
from app.api.metrics import router as metrics_router
|
2026-02-08 21:16:26 +05:30
|
|
|
from app.api.organizations import router as organizations_router
|
2026-02-13 23:11:54 +05:30
|
|
|
from app.api.skills_marketplace import router as skills_marketplace_router
|
2026-02-14 12:46:47 +05:30
|
|
|
from app.api.souls_directory import router as souls_directory_router
|
2026-02-12 18:35:48 +05:30
|
|
|
from app.api.tags import router as tags_router
|
2026-02-13 21:24:36 +05:30
|
|
|
from app.api.task_custom_fields import router as task_custom_fields_router
|
2026-02-04 02:28:51 +05:30
|
|
|
from app.api.tasks import router as tasks_router
|
2026-02-04 20:21:33 +05:30
|
|
|
from app.api.users import router as users_router
|
2026-02-01 22:25:28 +05:30
|
|
|
from app.core.config import settings
|
2026-02-07 03:14:30 +05:30
|
|
|
from app.core.error_handling import install_error_handling
|
2026-02-11 16:49:43 +05:30
|
|
|
from app.core.logging import configure_logging, get_logger
|
2026-03-04 02:15:14 -07:00
|
|
|
from app.core.rate_limit import validate_rate_limit_redis
|
|
|
|
|
from app.core.rate_limit_backend import RateLimitBackend
|
2026-02-24 17:36:44 +01:00
|
|
|
from app.core.security_headers import SecurityHeadersMiddleware
|
2026-02-01 23:16:56 +05:30
|
|
|
from app.db.session import init_db
|
2026-02-15 02:35:31 +05:30
|
|
|
from app.schemas.health import HealthStatusResponse
|
2026-02-01 22:25:28 +05:30
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from collections.abc import AsyncIterator
|
|
|
|
|
|
2026-02-02 20:51:08 +05:30
|
|
|
configure_logging()
|
2026-02-11 16:49:43 +05:30
|
|
|
logger = get_logger(__name__)
|
2026-02-13 02:08:30 +05:30
|
|
|
OPENAPI_TAGS = [
|
2026-02-15 02:32:05 +05:30
|
|
|
{
|
|
|
|
|
"name": "auth",
|
|
|
|
|
"description": (
|
|
|
|
|
"Authentication bootstrap endpoints for resolving caller identity and session context."
|
|
|
|
|
),
|
|
|
|
|
},
|
2026-02-15 02:22:28 +05:30
|
|
|
{
|
|
|
|
|
"name": "health",
|
|
|
|
|
"description": (
|
|
|
|
|
"Service liveness/readiness probes used by infrastructure and runtime checks."
|
|
|
|
|
),
|
|
|
|
|
},
|
2026-02-15 02:57:06 +05:30
|
|
|
{
|
|
|
|
|
"name": "agents",
|
|
|
|
|
"description": "Organization-level agent directory, lifecycle, and management operations.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "activity",
|
|
|
|
|
"description": "Activity feed and audit timeline endpoints across boards and operations.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "gateways",
|
|
|
|
|
"description": "Gateway management, synchronization, and runtime control operations.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "metrics",
|
|
|
|
|
"description": "Aggregated operational and board analytics metrics endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "organizations",
|
|
|
|
|
"description": "Organization profile, membership, and governance management endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "souls-directory",
|
|
|
|
|
"description": "Directory and lookup endpoints for agent soul templates and variants.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "skills",
|
|
|
|
|
"description": "Skills marketplace, install, uninstall, and synchronization endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "board-groups",
|
|
|
|
|
"description": "Board group CRUD, assignment, and grouping workflow endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "board-group-memory",
|
|
|
|
|
"description": "Shared memory endpoints scoped to board groups and grouped boards.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "boards",
|
|
|
|
|
"description": "Board lifecycle, configuration, and board-level management endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "board-memory",
|
|
|
|
|
"description": "Board-scoped memory read/write endpoints for persistent context.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "board-webhooks",
|
|
|
|
|
"description": "Board webhook registration, delivery config, and lifecycle endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "board-onboarding",
|
|
|
|
|
"description": "Board onboarding state, setup actions, and onboarding workflow endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "approvals",
|
|
|
|
|
"description": "Approval request, review, and status-tracking operations for board tasks.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "tasks",
|
|
|
|
|
"description": "Task CRUD, dependency management, and task workflow operations.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "custom-fields",
|
|
|
|
|
"description": "Organization custom-field definitions and board assignment endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "tags",
|
|
|
|
|
"description": "Tag catalog and task-tag association management endpoints.",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "users",
|
|
|
|
|
"description": "User profile read/update operations and user-centric settings endpoints.",
|
|
|
|
|
},
|
2026-02-13 02:08:30 +05:30
|
|
|
{
|
|
|
|
|
"name": "agent",
|
|
|
|
|
"description": (
|
|
|
|
|
"Agent-scoped API surface. All endpoints require `X-Agent-Token` and are "
|
|
|
|
|
"constrained by agent board access policies."
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "agent-lead",
|
|
|
|
|
"description": (
|
|
|
|
|
"Lead workflows: delegation, review orchestration, approvals, and "
|
|
|
|
|
"coordination actions."
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "agent-worker",
|
|
|
|
|
"description": (
|
|
|
|
|
"Worker workflows: task execution, task comments, and board/group context "
|
|
|
|
|
"reads/writes used during heartbeat loops."
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "agent-main",
|
|
|
|
|
"description": (
|
|
|
|
|
"Gateway-main control workflows that message board leads or broadcast "
|
|
|
|
|
"coordination requests."
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
]
|
2026-02-02 20:51:08 +05:30
|
|
|
|
2026-02-15 02:57:06 +05:30
|
|
|
_JSON_SCHEMA_REF_PREFIX = "#/components/schemas/"
|
|
|
|
|
_OPENAPI_EXAMPLE_TAGS = {
|
|
|
|
|
"agents",
|
|
|
|
|
"activity",
|
|
|
|
|
"gateways",
|
|
|
|
|
"metrics",
|
|
|
|
|
"organizations",
|
|
|
|
|
"souls-directory",
|
|
|
|
|
"skills",
|
|
|
|
|
"board-groups",
|
|
|
|
|
"board-group-memory",
|
|
|
|
|
"boards",
|
|
|
|
|
"board-memory",
|
|
|
|
|
"board-webhooks",
|
|
|
|
|
"board-onboarding",
|
|
|
|
|
"approvals",
|
|
|
|
|
"tasks",
|
|
|
|
|
"custom-fields",
|
|
|
|
|
"tags",
|
|
|
|
|
"users",
|
|
|
|
|
}
|
|
|
|
|
_GENERIC_RESPONSE_DESCRIPTIONS = {"Successful Response", "Validation Error"}
|
|
|
|
|
_HTTP_RESPONSE_DESCRIPTIONS = {
|
|
|
|
|
"200": "Request completed successfully.",
|
|
|
|
|
"201": "Resource created successfully.",
|
|
|
|
|
"202": "Request accepted for processing.",
|
|
|
|
|
"204": "Request completed successfully with no response body.",
|
|
|
|
|
"400": "Request validation failed.",
|
|
|
|
|
"401": "Authentication is required or token is invalid.",
|
|
|
|
|
"403": "Caller is authenticated but not authorized for this operation.",
|
|
|
|
|
"404": "Requested resource was not found.",
|
|
|
|
|
"409": "Request conflicts with the current resource state.",
|
|
|
|
|
"422": "Request payload failed schema or field validation.",
|
|
|
|
|
"429": "Request was rate-limited.",
|
|
|
|
|
"500": "Internal server error.",
|
|
|
|
|
}
|
|
|
|
|
_METHOD_SUMMARY_PREFIX = {
|
|
|
|
|
"get": "List",
|
|
|
|
|
"post": "Create",
|
|
|
|
|
"put": "Replace",
|
|
|
|
|
"patch": "Update",
|
|
|
|
|
"delete": "Delete",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_schema_ref(
|
|
|
|
|
schema: dict[str, Any],
|
|
|
|
|
*,
|
|
|
|
|
components: dict[str, Any],
|
|
|
|
|
seen_refs: set[str] | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""Resolve local component refs for OpenAPI schema traversal."""
|
|
|
|
|
ref = schema.get("$ref")
|
|
|
|
|
if not isinstance(ref, str):
|
|
|
|
|
return schema
|
|
|
|
|
if not ref.startswith(_JSON_SCHEMA_REF_PREFIX):
|
|
|
|
|
return schema
|
|
|
|
|
if seen_refs is None:
|
|
|
|
|
seen_refs = set()
|
|
|
|
|
if ref in seen_refs:
|
|
|
|
|
return schema
|
|
|
|
|
seen_refs.add(ref)
|
|
|
|
|
schema_name = ref[len(_JSON_SCHEMA_REF_PREFIX) :]
|
|
|
|
|
schemas = components.get("schemas")
|
|
|
|
|
if not isinstance(schemas, dict):
|
|
|
|
|
return schema
|
|
|
|
|
target = schemas.get(schema_name)
|
|
|
|
|
if not isinstance(target, dict):
|
|
|
|
|
return schema
|
|
|
|
|
return _resolve_schema_ref(target, components=components, seen_refs=seen_refs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _example_from_schema(schema: dict[str, Any], *, components: dict[str, Any]) -> Any:
|
|
|
|
|
"""Generate an OpenAPI example from schema metadata with sensible fallbacks."""
|
|
|
|
|
resolved = _resolve_schema_ref(schema, components=components)
|
|
|
|
|
|
|
|
|
|
if "example" in resolved:
|
|
|
|
|
return resolved["example"]
|
|
|
|
|
examples = resolved.get("examples")
|
|
|
|
|
if isinstance(examples, list) and examples:
|
|
|
|
|
return examples[0]
|
|
|
|
|
|
|
|
|
|
for composite_key in ("anyOf", "oneOf", "allOf"):
|
|
|
|
|
composite = resolved.get(composite_key)
|
|
|
|
|
if isinstance(composite, list):
|
|
|
|
|
for branch in composite:
|
|
|
|
|
if not isinstance(branch, dict):
|
|
|
|
|
continue
|
|
|
|
|
branch_example = _example_from_schema(branch, components=components)
|
|
|
|
|
if branch_example is not None:
|
|
|
|
|
return branch_example
|
|
|
|
|
|
|
|
|
|
enum_values = resolved.get("enum")
|
|
|
|
|
if isinstance(enum_values, list) and enum_values:
|
|
|
|
|
return enum_values[0]
|
|
|
|
|
|
|
|
|
|
schema_type = resolved.get("type")
|
|
|
|
|
if schema_type == "object":
|
|
|
|
|
output: dict[str, Any] = {}
|
|
|
|
|
properties = resolved.get("properties")
|
|
|
|
|
if isinstance(properties, dict):
|
|
|
|
|
for key, property_schema in properties.items():
|
|
|
|
|
if not isinstance(property_schema, dict):
|
|
|
|
|
continue
|
|
|
|
|
property_example = _example_from_schema(property_schema, components=components)
|
|
|
|
|
if property_example is not None:
|
|
|
|
|
output[key] = property_example
|
|
|
|
|
if output:
|
|
|
|
|
return output
|
|
|
|
|
additional_properties = resolved.get("additionalProperties")
|
|
|
|
|
if isinstance(additional_properties, dict):
|
|
|
|
|
value_example = _example_from_schema(additional_properties, components=components)
|
|
|
|
|
if value_example is not None:
|
|
|
|
|
return {"key": value_example}
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
if schema_type == "array":
|
|
|
|
|
items = resolved.get("items")
|
|
|
|
|
if isinstance(items, dict):
|
|
|
|
|
item_example = _example_from_schema(items, components=components)
|
|
|
|
|
if item_example is not None:
|
|
|
|
|
return [item_example]
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
if schema_type == "string":
|
|
|
|
|
return "string"
|
|
|
|
|
if schema_type == "integer":
|
|
|
|
|
return 0
|
|
|
|
|
if schema_type == "number":
|
|
|
|
|
return 0
|
|
|
|
|
if schema_type == "boolean":
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _inject_json_content_example(
|
|
|
|
|
*,
|
|
|
|
|
content: dict[str, Any],
|
|
|
|
|
components: dict[str, Any],
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Attach an example to application/json content when one is missing."""
|
|
|
|
|
app_json = content.get("application/json")
|
|
|
|
|
if not isinstance(app_json, dict):
|
|
|
|
|
return
|
|
|
|
|
if "example" in app_json or "examples" in app_json:
|
|
|
|
|
return
|
|
|
|
|
schema = app_json.get("schema")
|
|
|
|
|
if not isinstance(schema, dict):
|
|
|
|
|
return
|
|
|
|
|
generated_example = _example_from_schema(schema, components=components)
|
|
|
|
|
if generated_example is not None:
|
|
|
|
|
app_json["example"] = generated_example
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_operation_summary(*, method: str, path: str) -> str:
|
|
|
|
|
"""Build a readable summary when an operation does not define one."""
|
|
|
|
|
prefix = _METHOD_SUMMARY_PREFIX.get(method.lower(), "Handle")
|
|
|
|
|
path_without_prefix = path.removeprefix("/api/v1/")
|
|
|
|
|
parts = [
|
|
|
|
|
part.replace("-", " ")
|
|
|
|
|
for part in path_without_prefix.split("/")
|
|
|
|
|
if part and not (part.startswith("{") and part.endswith("}"))
|
|
|
|
|
]
|
|
|
|
|
if not parts:
|
|
|
|
|
return prefix
|
|
|
|
|
return f"{prefix} {' '.join(parts)}".strip().title()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_operation_docs(
|
|
|
|
|
*,
|
|
|
|
|
operation: dict[str, Any],
|
|
|
|
|
method: str,
|
|
|
|
|
path: str,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Normalize summary/description/responses/request-body docs for tagged operations."""
|
|
|
|
|
summary = str(operation.get("summary", "")).strip()
|
|
|
|
|
if not summary:
|
|
|
|
|
summary = _build_operation_summary(method=method, path=path)
|
|
|
|
|
operation["summary"] = summary
|
|
|
|
|
|
|
|
|
|
description = str(operation.get("description", "")).strip()
|
|
|
|
|
if not description:
|
|
|
|
|
operation["description"] = f"{summary}."
|
|
|
|
|
|
|
|
|
|
request_body = operation.get("requestBody")
|
|
|
|
|
if isinstance(request_body, dict):
|
|
|
|
|
if not str(request_body.get("description", "")).strip():
|
|
|
|
|
request_body["description"] = "JSON request payload."
|
|
|
|
|
|
|
|
|
|
responses = operation.get("responses")
|
|
|
|
|
if not isinstance(responses, dict):
|
|
|
|
|
return
|
|
|
|
|
for status_code, response in responses.items():
|
|
|
|
|
if not isinstance(response, dict):
|
|
|
|
|
continue
|
|
|
|
|
existing_description = str(response.get("description", "")).strip()
|
|
|
|
|
if not existing_description or existing_description in _GENERIC_RESPONSE_DESCRIPTIONS:
|
|
|
|
|
response["description"] = _HTTP_RESPONSE_DESCRIPTIONS.get(
|
|
|
|
|
str(status_code),
|
|
|
|
|
"Request processed.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _inject_tagged_operation_openapi_docs(openapi_schema: dict[str, Any]) -> None:
|
|
|
|
|
"""Ensure targeted-tag operations expose consistent OpenAPI docs and examples."""
|
|
|
|
|
components = openapi_schema.get("components")
|
|
|
|
|
if not isinstance(components, dict):
|
|
|
|
|
return
|
|
|
|
|
paths = openapi_schema.get("paths")
|
|
|
|
|
if not isinstance(paths, dict):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for path, path_item in paths.items():
|
|
|
|
|
if not isinstance(path_item, dict):
|
|
|
|
|
continue
|
|
|
|
|
for method, operation in path_item.items():
|
|
|
|
|
if not isinstance(operation, dict):
|
|
|
|
|
continue
|
|
|
|
|
tags = operation.get("tags")
|
|
|
|
|
if not isinstance(tags, list):
|
|
|
|
|
continue
|
|
|
|
|
if not _OPENAPI_EXAMPLE_TAGS.intersection(tags):
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
_normalize_operation_docs(operation=operation, method=method, path=path)
|
|
|
|
|
|
|
|
|
|
request_body = operation.get("requestBody")
|
|
|
|
|
if isinstance(request_body, dict):
|
|
|
|
|
request_content = request_body.get("content")
|
|
|
|
|
if isinstance(request_content, dict):
|
|
|
|
|
_inject_json_content_example(content=request_content, components=components)
|
|
|
|
|
|
|
|
|
|
responses = operation.get("responses")
|
|
|
|
|
if isinstance(responses, dict):
|
|
|
|
|
for response in responses.values():
|
|
|
|
|
if not isinstance(response, dict):
|
|
|
|
|
continue
|
|
|
|
|
response_content = response.get("content")
|
|
|
|
|
if isinstance(response_content, dict):
|
2026-02-15 03:02:54 +05:30
|
|
|
_inject_json_content_example(
|
|
|
|
|
content=response_content, components=components
|
|
|
|
|
)
|
2026-02-15 02:57:06 +05:30
|
|
|
|
2026-02-06 11:57:29 +05:30
|
|
|
|
2026-02-15 03:07:39 +05:30
|
|
|
def _build_custom_openapi(fastapi_app: FastAPI) -> dict[str, Any]:
|
|
|
|
|
"""Generate OpenAPI schema with normalized docs/examples for targeted tags."""
|
|
|
|
|
if fastapi_app.openapi_schema:
|
|
|
|
|
return fastapi_app.openapi_schema
|
|
|
|
|
openapi_schema = get_openapi(
|
|
|
|
|
title=fastapi_app.title,
|
|
|
|
|
version=fastapi_app.version,
|
|
|
|
|
openapi_version=fastapi_app.openapi_version,
|
|
|
|
|
description=fastapi_app.description,
|
|
|
|
|
routes=fastapi_app.routes,
|
|
|
|
|
tags=fastapi_app.openapi_tags,
|
|
|
|
|
servers=fastapi_app.servers,
|
|
|
|
|
)
|
|
|
|
|
_inject_tagged_operation_openapi_docs(openapi_schema)
|
|
|
|
|
fastapi_app.openapi_schema = openapi_schema
|
|
|
|
|
return fastapi_app.openapi_schema
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MissionControlFastAPI(FastAPI):
|
|
|
|
|
"""FastAPI application with custom OpenAPI normalization."""
|
|
|
|
|
|
|
|
|
|
def openapi(self) -> dict[str, Any]:
|
|
|
|
|
return _build_custom_openapi(self)
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 11:57:29 +05:30
|
|
|
@asynccontextmanager
|
2026-02-06 16:12:04 +05:30
|
|
|
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Initialize application resources before serving requests."""
|
2026-02-11 16:49:43 +05:30
|
|
|
logger.info(
|
|
|
|
|
"app.lifecycle.starting environment=%s db_auto_migrate=%s",
|
|
|
|
|
settings.environment,
|
|
|
|
|
settings.db_auto_migrate,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
await init_db()
|
2026-03-04 02:15:14 -07:00
|
|
|
if settings.rate_limit_backend == RateLimitBackend.REDIS:
|
|
|
|
|
validate_rate_limit_redis(settings.rate_limit_redis_url)
|
|
|
|
|
logger.info("app.lifecycle.rate_limit backend=redis")
|
|
|
|
|
else:
|
|
|
|
|
logger.info("app.lifecycle.rate_limit backend=memory")
|
2026-02-11 16:49:43 +05:30
|
|
|
logger.info("app.lifecycle.started")
|
|
|
|
|
try:
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
logger.info("app.lifecycle.stopped")
|
2026-02-06 11:57:29 +05:30
|
|
|
|
|
|
|
|
|
2026-02-15 03:07:39 +05:30
|
|
|
app = MissionControlFastAPI(
|
2026-02-13 02:08:30 +05:30
|
|
|
title="Mission Control API",
|
|
|
|
|
version="0.1.0",
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
openapi_tags=OPENAPI_TAGS,
|
|
|
|
|
)
|
2026-02-01 22:25:28 +05:30
|
|
|
|
|
|
|
|
origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
|
|
|
|
|
if origins:
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=origins,
|
|
|
|
|
allow_credentials=True,
|
2026-02-01 23:16:56 +05:30
|
|
|
allow_methods=["*"],
|
2026-02-01 22:25:28 +05:30
|
|
|
allow_headers=["*"],
|
2026-02-14 13:06:45 +05:30
|
|
|
expose_headers=["X-Total-Count", "X-Limit", "X-Offset"],
|
2026-02-01 22:25:28 +05:30
|
|
|
)
|
2026-02-11 16:49:43 +05:30
|
|
|
logger.info("app.cors.enabled origins_count=%s", len(origins))
|
|
|
|
|
else:
|
|
|
|
|
logger.info("app.cors.disabled")
|
2026-02-01 22:25:28 +05:30
|
|
|
|
2026-02-24 17:36:44 +01:00
|
|
|
app.add_middleware(
|
|
|
|
|
SecurityHeadersMiddleware,
|
|
|
|
|
x_content_type_options=settings.security_header_x_content_type_options,
|
|
|
|
|
x_frame_options=settings.security_header_x_frame_options,
|
|
|
|
|
referrer_policy=settings.security_header_referrer_policy,
|
|
|
|
|
permissions_policy=settings.security_header_permissions_policy,
|
|
|
|
|
)
|
2026-02-07 03:14:30 +05:30
|
|
|
install_error_handling(app)
|
|
|
|
|
|
2026-02-01 23:16:56 +05:30
|
|
|
|
2026-02-15 02:35:31 +05:30
|
|
|
@app.get(
|
|
|
|
|
"/health",
|
|
|
|
|
tags=["health"],
|
|
|
|
|
response_model=HealthStatusResponse,
|
|
|
|
|
summary="Health Check",
|
|
|
|
|
description="Lightweight liveness probe endpoint.",
|
|
|
|
|
responses={
|
|
|
|
|
status.HTTP_200_OK: {
|
|
|
|
|
"description": "Service is alive.",
|
|
|
|
|
"content": {"application/json": {"example": {"ok": True}}},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
def health() -> HealthStatusResponse:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Lightweight liveness probe endpoint."""
|
2026-02-15 02:35:31 +05:30
|
|
|
return HealthStatusResponse(ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get(
|
|
|
|
|
"/healthz",
|
|
|
|
|
tags=["health"],
|
|
|
|
|
response_model=HealthStatusResponse,
|
|
|
|
|
summary="Health Alias Check",
|
|
|
|
|
description="Alias liveness probe endpoint for platform compatibility.",
|
|
|
|
|
responses={
|
|
|
|
|
status.HTTP_200_OK: {
|
|
|
|
|
"description": "Service is alive.",
|
|
|
|
|
"content": {"application/json": {"example": {"ok": True}}},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
def healthz() -> HealthStatusResponse:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Alias liveness probe endpoint for platform compatibility."""
|
2026-02-15 02:35:31 +05:30
|
|
|
return HealthStatusResponse(ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get(
|
|
|
|
|
"/readyz",
|
|
|
|
|
tags=["health"],
|
|
|
|
|
response_model=HealthStatusResponse,
|
|
|
|
|
summary="Readiness Check",
|
|
|
|
|
description="Readiness probe endpoint for service orchestration checks.",
|
|
|
|
|
responses={
|
|
|
|
|
status.HTTP_200_OK: {
|
|
|
|
|
"description": "Service is ready.",
|
|
|
|
|
"content": {"application/json": {"example": {"ok": True}}},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
def readyz() -> HealthStatusResponse:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Readiness probe endpoint for service orchestration checks."""
|
2026-02-15 02:35:31 +05:30
|
|
|
return HealthStatusResponse(ok=True)
|
2026-02-04 20:21:33 +05:30
|
|
|
|
|
|
|
|
|
2026-02-04 02:28:51 +05:30
|
|
|
api_v1 = APIRouter(prefix="/api/v1")
|
|
|
|
|
api_v1.include_router(auth_router)
|
2026-02-05 19:06:32 +05:30
|
|
|
api_v1.include_router(agent_router)
|
2026-02-04 02:28:51 +05:30
|
|
|
api_v1.include_router(agents_router)
|
|
|
|
|
api_v1.include_router(activity_router)
|
|
|
|
|
api_v1.include_router(gateway_router)
|
2026-02-04 23:07:22 +05:30
|
|
|
api_v1.include_router(gateways_router)
|
2026-02-04 20:49:25 +05:30
|
|
|
api_v1.include_router(metrics_router)
|
2026-02-08 21:16:26 +05:30
|
|
|
api_v1.include_router(organizations_router)
|
2026-02-08 00:46:10 +05:30
|
|
|
api_v1.include_router(souls_directory_router)
|
2026-02-13 23:11:54 +05:30
|
|
|
api_v1.include_router(skills_marketplace_router)
|
2026-02-07 20:29:50 +05:30
|
|
|
api_v1.include_router(board_groups_router)
|
|
|
|
|
api_v1.include_router(board_group_memory_router)
|
2026-02-04 02:28:51 +05:30
|
|
|
api_v1.include_router(boards_router)
|
2026-02-05 14:43:25 +05:30
|
|
|
api_v1.include_router(board_memory_router)
|
2026-02-13 00:31:32 +05:30
|
|
|
api_v1.include_router(board_webhooks_router)
|
2026-02-05 14:43:25 +05:30
|
|
|
api_v1.include_router(board_onboarding_router)
|
|
|
|
|
api_v1.include_router(approvals_router)
|
2026-02-04 02:28:51 +05:30
|
|
|
api_v1.include_router(tasks_router)
|
2026-02-13 21:24:36 +05:30
|
|
|
api_v1.include_router(task_custom_fields_router)
|
2026-02-12 18:35:48 +05:30
|
|
|
api_v1.include_router(tags_router)
|
2026-02-04 20:21:33 +05:30
|
|
|
api_v1.include_router(users_router)
|
2026-02-04 02:28:51 +05:30
|
|
|
app.include_router(api_v1)
|
2026-02-06 19:11:11 +05:30
|
|
|
|
|
|
|
|
add_pagination(app)
|
2026-02-11 16:49:43 +05:30
|
|
|
logger.debug("app.routes.registered count=%s", len(app.routes))
|