systemd-deployed daemon that drives the semprini-core stack-support agent headless via Claude Code, monitors health via Uptime Kuma, applies safe upgrades, and escalates to the operator over Matrix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
6.3 KiB
Markdown
142 lines
6.3 KiB
Markdown
# semprini-maintainer — autonomous stack maintenance agent
|
|
|
|
A standalone systemd service that keeps the container stack healthy and current,
|
|
and talks to the operator over Matrix (chat.semprini.me).
|
|
|
|
It is the *unattended driver* for the canonical stack-support agent that lives
|
|
in the **semprini-core** repo (`agents/stack-support.agent.md`): the daemon
|
|
gathers state and hands the judgement calls (diagnosis, remediation, upgrade
|
|
planning) to that agent, run headless via Claude Code.
|
|
|
|
## Relationship to semprini-core
|
|
|
|
This is a **separate project** from the stack it maintains. It does not bundle
|
|
its own copy of the stack logic — at runtime it reads, from the semprini-core
|
|
checkout pointed to by `repo_dir` in `config.json`:
|
|
|
|
- `agents/stack-support.agent.md` — injected as the agent's system prompt
|
|
- `docs/stack-support-runbook.md` — the remediation runbook
|
|
- `core_stack/compose*.yml` — the compose files it operates on
|
|
|
|
So **a semprini-core checkout and the running core stack must be present on the
|
|
host** (they are what this agent maintains). The canonical stack-support agent
|
|
definition stays in semprini-core to avoid drift. Other projects that register
|
|
Uptime Kuma monitors (e.g. `semprini-blog`) are also maintained — their repos
|
|
are made available to the agent via `extra_dirs`.
|
|
|
|
### Prerequisites
|
|
|
|
- Docker (host user in the `docker` group)
|
|
- Python 3 (stdlib only — no venv needed)
|
|
- [Claude Code](https://claude.com/claude-code) CLI on `PATH`
|
|
- A semprini-core checkout + the core stack running (incl. Uptime Kuma)
|
|
|
|
## What it does
|
|
|
|
| Cadence | Action |
|
|
|---|---|
|
|
| **Every 5 min** (`health_interval_seconds`) | Read Uptime Kuma to learn which services are tracked **right now** and their up/down status. If a service is confirmed down across `down_confirmations` cycles, invoke the stack-support agent to diagnose + remediate, then report to Matrix. |
|
|
| **Every 7 days** (`upgrade_interval_seconds`) | Resolve the containers behind tracked services, find the owning compose file, and have the agent research + apply upgrades (test, pin, rollback on failure). Breaking/major upgrades are escalated to Matrix for a decision. |
|
|
| **Continuous** (`matrix_poll_seconds`) | Poll the Matrix room for the admin's replies — to answer escalations, or to take ad-hoc instructions. |
|
|
| **Daily** (`heartbeat_interval_seconds`) | Quiet "N/M services up" heartbeat when all is well. |
|
|
|
|
### Scope is dynamic
|
|
|
|
Scope is **whatever Uptime Kuma is tracking at the moment the check runs** — the
|
|
monitor list is re-read every cycle, nothing is hard-coded. This deliberately
|
|
covers services from *other* projects that register Kuma monitors (e.g.
|
|
`semprini-blog`), not just `semprini-core`. Other project repos are made
|
|
available to the agent via `extra_dirs`.
|
|
|
|
Kuma's own heartbeat result is the health signal, so the daemon never
|
|
re-implements probes — it trusts the monitoring system already in the stack.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ maintainer.py (daemon) │
|
|
Uptime Kuma ──┤ read monitors+status (sqlite in volume) │
|
|
(kuma.db) │ │
|
|
│ on down / on schedule: │
|
|
│ claude -p --append-system-prompt │──▶ stack-support
|
|
│ (agents/stack-support.agent.md) │ agent does the
|
|
│ │ diagnosis/fix
|
|
│ parse JSON result envelope │◀── + returns JSON
|
|
│ │
|
|
Matrix ◀─────┤ send summaries / ask questions │
|
|
(@paul) ─────┤ poll replies → resume escalations │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
The agent returns a small JSON envelope the daemon relays:
|
|
|
|
```json
|
|
{"status":"ok|action_taken|escalate|blocked","summary":"…","question":"…","options":["…"]}
|
|
```
|
|
|
|
`escalate` posts the question (numbered options) to Matrix and pauses upgrades
|
|
until the admin replies; the reply is fed back to the agent to carry out.
|
|
|
|
## Autonomy
|
|
|
|
Set `autonomy` in `config.json`:
|
|
|
|
- `full` *(default)* — remediate **and** apply safe upgrades autonomously
|
|
(test + auto-rollback); escalate only major/breaking upgrades.
|
|
- `fix-only` — remediate health issues; report upgrades but never apply them.
|
|
- `notify` — never change anything; only report to Matrix.
|
|
|
|
## Why it runs as `paul`, not root
|
|
|
|
- Claude Code refuses `--permission-mode bypassPermissions` under root.
|
|
- `paul` is in the `docker` group, so the agent can drive Docker/Compose.
|
|
- Kuma's root-owned `kuma.db` is read via a throwaway `keinos/sqlite3`
|
|
container mounting the named volume read-only — no host file access needed.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
cd /home/paul/Dev/semprini-maintainer
|
|
|
|
# 1. Config
|
|
cp config.example.json config.json
|
|
# set repo_dir to the semprini-core checkout; edit paths/intervals if needed
|
|
# (defaults match this host)
|
|
|
|
# 2. Provision the Matrix bot + private room with the admin
|
|
./register-matrix-bot.sh
|
|
# then, one-time: accept the room invite in Element as @paul:semprini.me
|
|
|
|
# 3. Install + start the service
|
|
./install.sh
|
|
```
|
|
|
|
`config.json` and `.bot-secrets` hold the bot token/password and are gitignored.
|
|
|
|
## Operate
|
|
|
|
```bash
|
|
journalctl -u semprini-maintainer -f # logs
|
|
systemctl status semprini-maintainer
|
|
sudo systemctl restart semprini-maintainer
|
|
|
|
# One-off cycles without the service (for testing):
|
|
python3 maintainer.py --config config.json --once health
|
|
python3 maintainer.py --config config.json --once upgrade
|
|
python3 maintainer.py --config config.json --once replies
|
|
```
|
|
|
|
## Files
|
|
|
|
| File | Purpose |
|
|
|---|---|
|
|
| `maintainer.py` | The daemon. Stdlib only. |
|
|
| `config.example.json` | Template — copy to `config.json`. |
|
|
| `register-matrix-bot.sh` | Creates the `@maintainer` account + room, writes token to config. |
|
|
| `semprini-maintainer.service` | systemd unit (runs as `paul`). |
|
|
| `install.sh` | Syntax-check, install, enable, start. |
|
|
|
|
See [`docs/architecture.md`](docs/architecture.md) for the architecture
|
|
decision record.
|