Require admin token for control endpoints

This commit is contained in:
2026-05-12 08:38:06 -05:00
parent 5b552351af
commit 0dc941a9a1
3 changed files with 26 additions and 10 deletions

View File

@@ -12,6 +12,7 @@ class Settings(BaseSettings):
APP_NAME: str = "Sanctum Chronicler" APP_NAME: str = "Sanctum Chronicler"
APP_ENV: str = "development" APP_ENV: str = "development"
DEBUG: bool = False DEBUG: bool = False
ADMIN_API_KEY: Optional[str] = None
@field_validator("DEBUG", mode="before") @field_validator("DEBUG", mode="before")
@classmethod @classmethod

View File

@@ -1,8 +1,9 @@
"""FastAPI main application.""" """FastAPI main application."""
import asyncio import asyncio
import secrets
from contextlib import suppress from contextlib import suppress
from fastapi import FastAPI, HTTPException, Form from fastapi import Depends, FastAPI, Form, Header, HTTPException
from datetime import datetime from datetime import datetime
import logging import logging
@@ -24,6 +25,19 @@ orchestrator: AgentOrchestrator | None = None
agent_loop_task: asyncio.Task | None = None agent_loop_task: asyncio.Task | None = None
async def require_admin(
admin_token: str | None = Header(default=None, alias="X-Admin-Token"),
) -> None:
"""Require the configured admin token for mutable/control endpoints."""
if not settings.ADMIN_API_KEY:
raise HTTPException(status_code=503, detail="Admin API key is not configured")
if not admin_token or not secrets.compare_digest(
admin_token,
settings.ADMIN_API_KEY,
):
raise HTTPException(status_code=401, detail="Invalid admin token")
async def agent_loop() -> None: async def agent_loop() -> None:
"""Run periodic time-based agent behavior for active sessions.""" """Run periodic time-based agent behavior for active sessions."""
if not orchestrator: if not orchestrator:
@@ -80,7 +94,7 @@ async def health_check() -> dict:
} }
@app.post("/admin/session/start") @app.post("/admin/session/start", dependencies=[Depends(require_admin)])
async def start_session(channel_name: str = Form(...)) -> dict: async def start_session(channel_name: str = Form(...)) -> dict:
"""Start a new stream session.""" """Start a new stream session."""
if not orchestrator: if not orchestrator:
@@ -95,7 +109,7 @@ async def start_session(channel_name: str = Form(...)) -> dict:
} }
@app.post("/admin/session/end") @app.post("/admin/session/end", dependencies=[Depends(require_admin)])
async def end_session(session_id: str = Form(...)) -> dict: async def end_session(session_id: str = Form(...)) -> dict:
"""End the current stream session.""" """End the current stream session."""
if not orchestrator: if not orchestrator:
@@ -109,7 +123,7 @@ async def end_session(session_id: str = Form(...)) -> dict:
} }
@app.post("/admin/test-message") @app.post("/admin/test-message", dependencies=[Depends(require_admin)])
async def test_message(session_id: str = Form(...), message: str = Form(...), username: str = Form("test_user")) -> dict: async def test_message(session_id: str = Form(...), message: str = Form(...), username: str = Form("test_user")) -> dict:
"""Send a test message to the orchestrator.""" """Send a test message to the orchestrator."""
if not orchestrator: if not orchestrator:
@@ -128,7 +142,7 @@ async def test_message(session_id: str = Form(...), message: str = Form(...), us
} }
@app.post("/admin/test-agent-response") @app.post("/admin/test-agent-response", dependencies=[Depends(require_admin)])
async def test_agent_response( async def test_agent_response(
session_id: str = Form(...), session_id: str = Form(...),
message: str = Form(...), message: str = Form(...),
@@ -153,7 +167,7 @@ async def test_agent_response(
} }
@app.post("/admin/test-loop-inactivity") @app.post("/admin/test-loop-inactivity", dependencies=[Depends(require_admin)])
async def test_loop_inactivity( async def test_loop_inactivity(
session_id: str = Form(...), session_id: str = Form(...),
inactive_minutes: int = Form(16), inactive_minutes: int = Form(16),
@@ -176,7 +190,7 @@ async def test_loop_inactivity(
} }
@app.get("/admin/ledger") @app.get("/admin/ledger", dependencies=[Depends(require_admin)])
async def get_ledger(session_id: str) -> dict: async def get_ledger(session_id: str) -> dict:
"""Get the markdown ledger for a session.""" """Get the markdown ledger for a session."""
if not orchestrator: if not orchestrator:
@@ -192,7 +206,7 @@ async def get_ledger(session_id: str) -> dict:
} }
@app.get("/admin/session/status") @app.get("/admin/session/status", dependencies=[Depends(require_admin)])
async def get_session_status(session_id: str) -> dict: async def get_session_status(session_id: str) -> dict:
"""Get status for an active stream session.""" """Get status for an active stream session."""
if not orchestrator: if not orchestrator:
@@ -208,7 +222,7 @@ async def get_session_status(session_id: str) -> dict:
} }
@app.get("/admin/loop/status") @app.get("/admin/loop/status", dependencies=[Depends(require_admin)])
async def get_loop_status() -> dict: async def get_loop_status() -> dict:
"""Get the background agent loop runtime configuration.""" """Get the background agent loop runtime configuration."""
if not orchestrator: if not orchestrator:
@@ -221,7 +235,7 @@ async def get_loop_status() -> dict:
} }
@app.post("/admin/loop/frequency") @app.post("/admin/loop/frequency", dependencies=[Depends(require_admin)])
async def set_loop_frequency(interval_seconds: float = Form(...)) -> dict: async def set_loop_frequency(interval_seconds: float = Form(...)) -> dict:
"""Set how frequently the background agent loop runs.""" """Set how frequently the background agent loop runs."""
if not orchestrator: if not orchestrator:

View File

@@ -32,6 +32,7 @@ services:
APP_NAME: "Sanctum Chronicler" APP_NAME: "Sanctum Chronicler"
APP_ENV: ${APP_ENV:-development} APP_ENV: ${APP_ENV:-development}
DEBUG: ${DEBUG:-false} DEBUG: ${DEBUG:-false}
ADMIN_API_KEY: ${ADMIN_API_KEY:-}
DATABASE_URL: postgresql+asyncpg://sanctum:${DB_PASSWORD:-password}@sanctum-db:5432/sanctum DATABASE_URL: postgresql+asyncpg://sanctum:${DB_PASSWORD:-password}@sanctum-db:5432/sanctum
TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID:-} TWITCH_CLIENT_ID: ${TWITCH_CLIENT_ID:-}
TWITCH_CLIENT_SECRET: ${TWITCH_CLIENT_SECRET:-} TWITCH_CLIENT_SECRET: ${TWITCH_CLIENT_SECRET:-}