MosswartOverlord/agent/README.md
Erik 79cf88d3f7 feat(agent): Phase 1 — chat-window AI assistant via Claude Code subprocess
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>
2026-04-25 20:43:59 +02:00

146 lines
5.4 KiB
Markdown

# Overlord Agent
A small host-side Python service that gives Claude Code (running in
headless mode) access to live Overlord data so it can answer questions
from the dashboard chat window.
## Why a separate service?
`dereth-tracker` runs in Docker. The `claude` CLI binary at
`/home/erik/.local/bin/claude` depends on `~/.claude` credentials owned
by user `erik` on the host. The tracker container can't invoke it.
So this service runs **outside** Docker, listens on `127.0.0.1:8767`,
and nginx routes `/api/agent/*` to it. It validates the same browser
session cookie the tracker issues (shared `SECRET_KEY`) and shells out
to `claude -p` with `cwd=/home/erik/MosswartOverlord`.
## Architecture
```
Browser ──nginx──┬─► /api/* ──► dereth-tracker (Docker, 8765)
└─► /api/agent/* ──► overlord-agent (host, 8767)
├─► subprocess: claude -p ...
│ │
│ └─► MCP stdio ──► mcp_overlord.py
│ │
│ └─► HTTP loopback to tracker
│ └─► asyncpg to dereth-db
└─► validates "session" cookie
```
## Files
| File | What |
|------|------|
| `service.py` | FastAPI app (`/agent/health`, `/agent/sessions/new`, `/agent/ask`, `/agent/sessions/{id}/history`) |
| `auth.py` | Session-cookie validation (mirrors `main.py:1013-1019`) |
| `claude_wrapper.py` | `asyncio.create_subprocess_exec("claude", "-p", ...)` |
| `tools.py` | Pure tool implementations (HTTP loopback + read-only DB) |
| `mcp_overlord.py` | MCP stdio server registering tools for Claude Code |
| `sql/0001_overlord_agent_ro.sql` | Read-only PG role for the SQL tool |
| `overlord-agent.service` | systemd unit |
| `install.sh` | One-shot installer (venv + pip install + systemd) |
## Required env vars (in repo-root `.env`)
```
SECRET_KEY=<same value the tracker uses to sign cookies>
AGENT_DB_DSN=postgresql://overlord_agent_ro:<password>@127.0.0.1:5432/dereth
TRACKER_URL=http://127.0.0.1:8765 # optional, this is the default
CLAUDE_BIN=/home/erik/.local/bin/claude # optional, this is the default
CLAUDE_CWD=/home/erik/MosswartOverlord # optional, this is the default
CLAUDE_TIMEOUT_S=120 # optional
```
## First-time setup on the server
1. **Create the read-only DB role** (one-time):
```bash
docker exec -i dereth-db psql -U postgres -d dereth \
< /home/erik/MosswartOverlord/agent/sql/0001_overlord_agent_ro.sql
docker exec -it dereth-db psql -U postgres -d dereth \
-c "ALTER ROLE overlord_agent_ro PASSWORD '<random-password>';"
```
2. **Add `AGENT_DB_DSN`** to `/home/erik/MosswartOverlord/.env` with the
password you just set.
3. **Run the installer**:
```bash
cd /home/erik/MosswartOverlord
bash agent/install.sh
```
4. **Update nginx**: edit `/etc/nginx/sites-enabled/overlord` to add the
`/api/agent/` location (already in `nginx/overlord.conf` in the repo —
just `sudo cp` and reload).
## Day-to-day deploy
After editing any agent file:
```bash
# On dev:
git push
# On server:
ssh erik@overlord.snakedesert.se
cd /home/erik/MosswartOverlord
git pull
sudo systemctl restart overlord-agent
journalctl -u overlord-agent -f # tail logs
```
For Python dependency changes:
```bash
agent/.venv/bin/pip install -r agent/requirements.txt
sudo systemctl restart overlord-agent
```
## Smoke tests
```bash
# 1. Service alive?
curl http://127.0.0.1:8767/agent/health
# 2. Cookie required?
curl -X POST http://127.0.0.1:8767/agent/ask \
-H 'Content-Type: application/json' \
-d '{"session_id":"x","message":"hi"}'
# ⇒ 401
# 3. Direct claude invocation works?
echo "hello" | /home/erik/.local/bin/claude -p \
--session-id 11111111-1111-1111-1111-111111111111 \
--output-format json
# 4. End-to-end via nginx (with cookie):
curl -X POST https://overlord.snakedesert.se/api/agent/ask \
-b 'session=<your-session-cookie>' \
-H 'Content-Type: application/json' \
-d '{"session_id":"<uuid>","message":"How many characters are online?"}'
```
## Cost / rate-limit notes
- Each `/agent/ask` shells out to `claude -p` once.
- We use the user's Claude subscription (no API key) — flat-rate, no
per-call billing, but subscription-tier rate limits still apply.
- **Reactive only**: there are no background loops or periodic ticks.
Each user message = one Claude turn (which may chain several tool
calls internally before producing a final answer).
- The SQL tool is hard-capped at 10s and 200 rows.
- `suitbuilder_search` is the only tool that can take minutes; nginx
read timeout is 180s for `/api/agent/`.
## Adding a new MCP tool
1. Implement `async def my_tool(...) -> dict` in `tools.py`.
2. Register it in `mcp_overlord.py` under `TOOL_DEFS`:
- description (the agent reads this to decide when to call)
- JSON schema for arguments
- lambda dispatching to `T.my_tool(...)`
3. `sudo systemctl restart overlord-agent`. Claude Code re-discovers the
tool list on each invocation.