Adds an in-dashboard AI assistant that answers questions about live game
state. Designed reactively (no background loops) — every user message in
the chat window or via /api/agent/ask runs one `claude -p` invocation.
Architecture:
- New host-side FastAPI service (agent/) on 127.0.0.1:8767, OUTSIDE the
dereth-tracker Docker container because `claude` and ~/.claude
credentials live on the host.
- nginx routes /api/agent/* to the host service.
- The same browser session cookie the tracker issues authenticates
agent requests (shared SECRET_KEY).
- The agent shells out to `claude -p --session-id <uuid>` with
cwd=/home/erik/MosswartOverlord. Sessions persist as JSONL on disk
via Claude Code's built-in machinery.
- An MCP stdio server (agent/mcp_overlord.py) exposes tools to Claude:
get_live_players, get_recent_rares, query_telemetry_db (read-only,
parsed by sqlglot to reject DML/DDL), get_player_state, get_inventory,
get_inventory_search, get_combat_stats, get_equipment_cantrips,
get_quest_status, get_server_health, suitbuilder_search.
- Read-only PG role (overlord_agent_ro) is the second line of defense
on the SQL tool — even a parser bypass can't mutate.
Frontend:
- AgentWindow.tsx — draggable chat window with localStorage-pinned
session UUID, "New Chat" button, on-mount rehydration from
/agent/sessions/{id}/history (parses Claude Code's JSONL).
- Wired into WindowRenderer + Sidebar (🤖 Assistant button).
Operational:
- systemd unit (overlord-agent.service) + install.sh.
- agent/README.md documents env vars, deploy flow, smoke tests.
- nginx/overlord.conf gets a new /api/agent/ location with 180s timeout.
- CLAUDE.md gets an "Overlord Assistant Mode" section briefing the
agent on which tools to use and how to behave.
NOT YET DEPLOYED — server still needs:
1. Apply agent/sql/0001_overlord_agent_ro.sql + ALTER ROLE password
2. Add AGENT_DB_DSN to /home/erik/MosswartOverlord/.env
3. bash agent/install.sh (creates venv, installs unit, starts service)
4. sudo cp /home/erik/MosswartOverlord/nginx/overlord.conf to nginx + reload
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
51 lines
1.6 KiB
Python
51 lines
1.6 KiB
Python
"""Session-cookie validation that mirrors main.py.
|
|
|
|
Re-implements the verify path so this host-side service can authenticate
|
|
the same browser cookie that dereth-tracker issues. Both services must
|
|
share the SECRET_KEY env var.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from fastapi import HTTPException, Request, status
|
|
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
|
|
|
|
# Mirror main.py:996-998
|
|
SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production-please")
|
|
SESSION_MAX_AGE = 30 * 24 * 3600 # 30 days
|
|
_serializer = URLSafeTimedSerializer(SECRET_KEY)
|
|
|
|
|
|
def verify_session_cookie(token: str) -> dict | None:
|
|
"""Verify and decode a session token. Returns None if invalid/expired.
|
|
|
|
Mirrors main.py:1013-1019 byte-for-byte so a cookie issued by the tracker
|
|
decodes here identically.
|
|
"""
|
|
try:
|
|
data = _serializer.loads(token, max_age=SESSION_MAX_AGE)
|
|
return {"username": data["u"], "is_admin": data["a"]}
|
|
except (BadSignature, SignatureExpired, KeyError):
|
|
return None
|
|
|
|
|
|
def require_user(request: Request) -> dict:
|
|
"""FastAPI dependency: enforces a valid session cookie.
|
|
|
|
Returns the decoded user dict on success; raises 401 otherwise.
|
|
"""
|
|
token = request.cookies.get("session")
|
|
if not token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Not authenticated",
|
|
)
|
|
user = verify_session_cookie(token)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Session invalid or expired",
|
|
)
|
|
return user
|