2 Commits

Author SHA1 Message Date
Abhimanyu Saharan
b82c9960c3 Merge branch 'master' into docs/openapi-security-errors 2026-02-14 03:15:30 +05:30
Abhimanyu Saharan
d5c72f3592 Docs/OpenAPI: add AgentToken security scheme + standard auth errors 2026-02-12 14:18:20 +00:00
3 changed files with 262 additions and 0 deletions

112
backend/app/core/openapi.py Normal file
View 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

View File

@@ -94,6 +94,16 @@ app = FastAPI(
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()]
if origins:
app.add_middleware(

140
docs/07-api-reference.md Normal file
View File

@@ -0,0 +1,140 @@
# API / auth
This page documents how Mission Controls 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 agents 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 dont “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`