Compare commits
2 Commits
abhi1693/f
...
docs/opena
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b82c9960c3 | ||
|
|
d5c72f3592 |
112
backend/app/core/openapi.py
Normal file
112
backend/app/core/openapi.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""OpenAPI customization helpers.
|
||||||
|
|
||||||
|
Goal: make the generated OpenAPI spec accurately represent Mission Control auth modes.
|
||||||
|
|
||||||
|
Mission Control supports two primary auth mechanisms:
|
||||||
|
- User (Clerk): Authorization: Bearer <token> (HTTP bearer)
|
||||||
|
- Agent: X-Agent-Token: <token> (apiKey in header)
|
||||||
|
|
||||||
|
FastAPI's default OpenAPI generation only reflects dependencies that use built-in
|
||||||
|
security helpers. For agent auth, we add an explicit `AgentToken` securityScheme
|
||||||
|
and annotate operations that accept `X-Agent-Token`.
|
||||||
|
|
||||||
|
This module is intentionally *documentation-only*: it must not change runtime
|
||||||
|
authentication behavior.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Final
|
||||||
|
|
||||||
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
|
||||||
|
AGENT_TOKEN_HEADER: Final[str] = "X-Agent-Token"
|
||||||
|
AGENT_TOKEN_SCHEME_NAME: Final[str] = "AgentToken"
|
||||||
|
|
||||||
|
|
||||||
|
def _has_header_param(op: dict[str, Any], header_name: str) -> bool:
|
||||||
|
for p in op.get("parameters", []) or []:
|
||||||
|
if p.get("in") == "header" and p.get("name") == header_name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def build_openapi(app_title: str, app_version: str, routes: Any) -> dict[str, Any]:
|
||||||
|
"""Return a customized OpenAPI document.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_title: FastAPI app title.
|
||||||
|
app_version: FastAPI app version.
|
||||||
|
routes: FastAPI routes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OpenAPI dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
schema: dict[str, Any] = get_openapi(title=app_title, version=app_version, routes=routes)
|
||||||
|
|
||||||
|
components = schema.setdefault("components", {})
|
||||||
|
security_schemes = components.setdefault("securitySchemes", {})
|
||||||
|
|
||||||
|
# Add AgentToken scheme (apiKey header).
|
||||||
|
security_schemes.setdefault(
|
||||||
|
AGENT_TOKEN_SCHEME_NAME,
|
||||||
|
{"type": "apiKey", "in": "header", "name": AGENT_TOKEN_HEADER},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a small, explicit error schema we can reference for 401/403.
|
||||||
|
schemas = components.setdefault("schemas", {})
|
||||||
|
schemas.setdefault(
|
||||||
|
"HTTPError",
|
||||||
|
{
|
||||||
|
"title": "HTTPError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"detail": {"title": "Detail", "anyOf": [{"type": "string"}, {"type": "object"}]}},
|
||||||
|
"required": ["detail"],
|
||||||
|
"description": "Standard FastAPI HTTPException response body.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def ensure_auth_responses(op: dict[str, Any]) -> None:
|
||||||
|
responses = op.setdefault("responses", {})
|
||||||
|
for code, desc in (("401", "Unauthorized"), ("403", "Forbidden")):
|
||||||
|
responses.setdefault(
|
||||||
|
code,
|
||||||
|
{
|
||||||
|
"description": desc,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/HTTPError"},
|
||||||
|
"examples": {
|
||||||
|
"example": {"value": {"detail": desc}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Walk operations and attach AgentToken security where relevant.
|
||||||
|
for _path, methods in (schema.get("paths") or {}).items():
|
||||||
|
for _method, op in (methods or {}).items():
|
||||||
|
if not isinstance(op, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_agent_header = _has_header_param(op, AGENT_TOKEN_HEADER)
|
||||||
|
|
||||||
|
# If the operation already declares bearer security, and it also accepts agent token,
|
||||||
|
# represent "either bearer OR agent token".
|
||||||
|
if has_agent_header:
|
||||||
|
security = op.get("security") or []
|
||||||
|
if security:
|
||||||
|
# OR across entries (list). Preserve existing, add agent token as another option.
|
||||||
|
if not any(AGENT_TOKEN_SCHEME_NAME in s for s in security if isinstance(s, dict)):
|
||||||
|
security.append({AGENT_TOKEN_SCHEME_NAME: []})
|
||||||
|
op["security"] = security
|
||||||
|
else:
|
||||||
|
op["security"] = [{AGENT_TOKEN_SCHEME_NAME: []}]
|
||||||
|
|
||||||
|
# If the operation requires *any* auth (bearer and/or agent token), document 401/403.
|
||||||
|
if op.get("security"):
|
||||||
|
ensure_auth_responses(op)
|
||||||
|
|
||||||
|
return schema
|
||||||
@@ -94,6 +94,16 @@ app = FastAPI(
|
|||||||
openapi_tags=OPENAPI_TAGS,
|
openapi_tags=OPENAPI_TAGS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# OpenAPI customization (docs-only): reflect agent auth (X-Agent-Token) in the spec.
|
||||||
|
from app.core.openapi import build_openapi # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def custom_openapi() -> dict:
|
||||||
|
return build_openapi(app_title=app.title, app_version=app.version, routes=app.routes)
|
||||||
|
|
||||||
|
|
||||||
|
app.openapi = custom_openapi # type: ignore[assignment]
|
||||||
|
|
||||||
origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
|
origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
|
||||||
if origins:
|
if origins:
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
|
|||||||
140
docs/07-api-reference.md
Normal file
140
docs/07-api-reference.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# API / auth
|
||||||
|
|
||||||
|
This page documents how Mission Control’s API surface is organized and how authentication works.
|
||||||
|
|
||||||
|
For deeper backend architecture context, see:
|
||||||
|
- [Architecture](05-architecture.md)
|
||||||
|
|
||||||
|
## Base path
|
||||||
|
|
||||||
|
Evidence: `backend/app/main.py`.
|
||||||
|
|
||||||
|
- All API routes are mounted under: `/api/v1/*`
|
||||||
|
|
||||||
|
## OpenAPI / Swagger UI
|
||||||
|
|
||||||
|
Mission Control is a FastAPI app, so it serves its OpenAPI schema and interactive docs:
|
||||||
|
|
||||||
|
- OpenAPI JSON: `GET /openapi.json`
|
||||||
|
- Swagger UI: `GET /docs`
|
||||||
|
- ReDoc: `GET /redoc`
|
||||||
|
|
||||||
|
### Exporting OpenAPI to a versioned artifact
|
||||||
|
|
||||||
|
Evidence: `backend/scripts/export_openapi.py`, `backend/README.md`.
|
||||||
|
|
||||||
|
From repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
uv sync --extra dev
|
||||||
|
uv run python scripts/export_openapi.py
|
||||||
|
# writes: backend/openapi.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth model (two callers)
|
||||||
|
|
||||||
|
Mission Control has two primary actor types:
|
||||||
|
|
||||||
|
1) **User (Clerk)** — human UI/admin
|
||||||
|
2) **Agent (`X-Agent-Token`)** — automation
|
||||||
|
|
||||||
|
### User auth (Clerk)
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- backend: `backend/app/core/auth.py`
|
||||||
|
- config: `backend/app/core/config.py`
|
||||||
|
|
||||||
|
- Frontend calls backend using `Authorization: Bearer <token>`.
|
||||||
|
- Backend validates requests using the Clerk Backend API SDK with `CLERK_SECRET_KEY`.
|
||||||
|
|
||||||
|
### Agent auth (`X-Agent-Token`)
|
||||||
|
|
||||||
|
Evidence:
|
||||||
|
- `backend/app/core/agent_auth.py`
|
||||||
|
- agent API surface: `backend/app/api/agent.py`
|
||||||
|
|
||||||
|
- Agents authenticate with `X-Agent-Token: <token>`.
|
||||||
|
- Token is verified against the agent’s stored `agent_token_hash`.
|
||||||
|
|
||||||
|
OpenAPI note:
|
||||||
|
- The OpenAPI schema models this as an `apiKey` security scheme named `AgentToken`.
|
||||||
|
- Some endpoints allow **either** `Authorization: Bearer ...` (admin user) **or** `X-Agent-Token` (agent).
|
||||||
|
|
||||||
|
## Route groups (modules)
|
||||||
|
|
||||||
|
Evidence: `backend/app/main.py` includes routers from `backend/app/api/*`.
|
||||||
|
|
||||||
|
| Module | Prefix (under `/api/v1`) | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `activity.py` | `/activity` | Activity listing and task-comment feed endpoints. |
|
||||||
|
| `agent.py` | `/agent` | Agent-scoped API routes for board operations and gateway coordination. |
|
||||||
|
| `agents.py` | `/agents` | Thin API wrappers for async agent lifecycle operations. |
|
||||||
|
| `approvals.py` | `/boards/{board_id}/approvals` | Approval listing, streaming, creation, and update endpoints. |
|
||||||
|
| `auth.py` | `/auth` | Authentication bootstrap endpoints for the Mission Control API. |
|
||||||
|
| `board_group_memory.py` | `/board-groups/{group_id}/memory` and `/boards/{board_id}/group-memory` | Board-group memory CRUD and streaming endpoints. |
|
||||||
|
| `board_groups.py` | `/board-groups` | Board group CRUD, snapshot, and heartbeat endpoints. |
|
||||||
|
| `board_memory.py` | `/boards/{board_id}/memory` | Board memory CRUD and streaming endpoints. |
|
||||||
|
| `board_onboarding.py` | `/boards/{board_id}/onboarding` | Board onboarding endpoints for user/agent collaboration. |
|
||||||
|
| `boards.py` | `/boards` | Board CRUD and snapshot endpoints. |
|
||||||
|
| `gateway.py` | `/gateways` | Thin gateway session-inspection API wrappers. |
|
||||||
|
| `gateways.py` | `/gateways` | Thin API wrappers for gateway CRUD and template synchronization. |
|
||||||
|
| `metrics.py` | `/metrics` | Dashboard metric aggregation endpoints. |
|
||||||
|
| `organizations.py` | `/organizations` | Organization management endpoints and membership/invite flows. |
|
||||||
|
| `souls_directory.py` | `/souls-directory` | API routes for searching and fetching souls-directory markdown entries. |
|
||||||
|
| `tasks.py` | `/boards/{board_id}/tasks` | Task API routes for listing, streaming, and mutating board tasks. |
|
||||||
|
| `users.py` | `/users` | User self-service API endpoints for profile retrieval and updates. |
|
||||||
|
|
||||||
|
## Backend API layer notes (how modules are organized)
|
||||||
|
|
||||||
|
Evidence: `backend/app/main.py`, `backend/app/api/*`, `backend/app/api/deps.py`.
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
|
||||||
|
- Each file under `backend/app/api/*` typically declares an `APIRouter` (`router = APIRouter(...)`) and defines endpoints with decorators like `@router.get(...)`, `@router.post(...)`, etc.
|
||||||
|
- Board-scoped modules embed `{board_id}` in the prefix (e.g. `/boards/{board_id}/tasks`).
|
||||||
|
- Streaming endpoints usually expose **SSE** endpoints at `.../stream` (see `sse-starlette` usage).
|
||||||
|
|
||||||
|
### Where key behaviors live
|
||||||
|
|
||||||
|
- **Router wiring / base prefix**: `backend/app/main.py` mounts these routers under `/api/v1/*`.
|
||||||
|
- **Auth / access control** is mostly expressed through dependencies (see `backend/app/api/deps.py`):
|
||||||
|
- `require_admin_auth` — require an authenticated *admin user*.
|
||||||
|
- `require_admin_or_agent` — allow either an admin user or an authenticated agent.
|
||||||
|
- `get_board_for_actor_read` / `get_board_for_actor_write` — enforce board access for the calling actor.
|
||||||
|
- `require_org_member` / `require_org_admin` — enforce org membership/admin for user callers.
|
||||||
|
- **Agent-only surface**: `backend/app/api/agent.py` uses `get_agent_auth_context` (X-Agent-Token) and contains board/task/memory endpoints specifically for automation.
|
||||||
|
|
||||||
|
### Module-by-module map (prefix, key endpoints, and pointers)
|
||||||
|
|
||||||
|
This is a “where to look” index, not a full OpenAPI dump. For exact parameters and response shapes, see:
|
||||||
|
- route module file (`backend/app/api/<module>.py`)
|
||||||
|
- schemas (`backend/app/schemas/*`)
|
||||||
|
- models (`backend/app/models/*`)
|
||||||
|
- services (`backend/app/services/*`)
|
||||||
|
|
||||||
|
| Module | Prefix (under `/api/v1`) | Key endpoints (examples) | Main deps / auth | Pointers (schemas/models/services) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `activity.py` | `/activity` | `GET /activity` (events); `GET /activity/task-comments` + `/stream` | `require_admin_or_agent`, `require_org_member` | `app/models/activity_events.py`, `app/schemas/activity_events.py` |
|
||||||
|
| `agent.py` | `/agent` | agent automation surface: boards/tasks/memory + gateway coordination | `get_agent_auth_context` (X-Agent-Token) | `backend/app/core/agent_auth.py`, `backend/app/services/openclaw/*` |
|
||||||
|
| `agents.py` | `/agents` | agent lifecycle + SSE stream + heartbeat | org-admin gated for user callers; some endpoints allow agent access via deps | `app/schemas/agents.py`, `app/services/openclaw/provisioning_db.py` |
|
||||||
|
| `approvals.py` | `/boards/{board_id}/approvals` | list/create/update approvals + `/stream` | `require_admin_or_agent` + board access deps | `app/models/approvals.py`, `app/schemas/approvals.py` |
|
||||||
|
|
||||||
|
## Where authorization is enforced
|
||||||
|
|
||||||
|
Evidence: `backend/app/api/deps.py`.
|
||||||
|
|
||||||
|
Most route modules don’t “hand roll” access checks; they declare dependencies:
|
||||||
|
|
||||||
|
- `require_admin_auth` — admin user only.
|
||||||
|
- `require_admin_or_agent` — admin user OR authenticated agent.
|
||||||
|
- `get_board_for_actor_read` / `get_board_for_actor_write` — board access for user/agent.
|
||||||
|
- `require_org_member` / `require_org_admin` — org membership/admin for user callers.
|
||||||
|
|
||||||
|
## “Start here” pointers for maintainers
|
||||||
|
|
||||||
|
- Router wiring: `backend/app/main.py`
|
||||||
|
- Access dependencies: `backend/app/api/deps.py`
|
||||||
|
- User auth: `backend/app/core/auth.py`
|
||||||
|
- Agent auth: `backend/app/core/agent_auth.py`
|
||||||
|
- Agent automation surface: `backend/app/api/agent.py`
|
||||||
Reference in New Issue
Block a user