The query_telemetry_db tool was crashing with AttributeError because exp.AlterTable doesn't exist in this sqlglot version (renamed to Alter). Made the deny-class list build defensively via getattr and dropped any classes that the installed sqlglot doesn't expose. Also broadened the deny list (Alter, AlterColumn, AlterDatabase, Truncate, Grant, Revoke, Copy) and made the toplevel allowlist tolerant of missing classes too. The walk() return shape is also normalized in case sqlglot versions yield (node, parent, key) tuples vs. bare nodes. Belt-and-suspenders is fine — the GRANT-SELECT-only PG role is the real write barrier; the parser is just a faster/friendlier reject path. |
||
|---|---|---|
| .. | ||
| sql | ||
| __init__.py | ||
| auth.py | ||
| claude_wrapper.py | ||
| install.sh | ||
| mcp_overlord.py | ||
| overlord-agent.service | ||
| README.md | ||
| requirements.txt | ||
| service.py | ||
| tools.py | ||
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
- Create the read-only DB role (one-time):
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>';" - Add
AGENT_DB_DSNto/home/erik/MosswartOverlord/.envwith the password you just set. - Run the installer:
cd /home/erik/MosswartOverlord bash agent/install.sh - Update nginx: edit
/etc/nginx/sites-enabled/overlordto add the/api/agent/location (already innginx/overlord.confin the repo — justsudo cpand reload).
Day-to-day deploy
After editing any agent file:
# 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:
agent/.venv/bin/pip install -r agent/requirements.txt
sudo systemctl restart overlord-agent
Smoke tests
# 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/askshells out toclaude -ponce. - 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_searchis the only tool that can take minutes; nginx read timeout is 180s for/api/agent/.
Adding a new MCP tool
- Implement
async def my_tool(...) -> dictintools.py. - Register it in
mcp_overlord.pyunderTOOL_DEFS:- description (the agent reads this to decide when to call)
- JSON schema for arguments
- lambda dispatching to
T.my_tool(...)
sudo systemctl restart overlord-agent. Claude Code re-discovers the tool list on each invocation.