- Implemented `register-gitea-bot.sh` to provision Gitea access token. - Updated `maintainer.py` to support Gitea API for push and PR operations. - Modified `install.sh` to warn if Gitea token is not provisioned. - Enhanced documentation in `README.md`, `CLAUDE.md`, and `architecture.md` to reflect new Gitea functionality. - Added Gitea configuration to `config.example.json`.
3.9 KiB
3.9 KiB
Maintenance Agent (semprini-maintainer)
Autonomous, always-on agent that keeps the container stack healthy and current.
Implementation: this repository — see README.md. It maintains
the semprini-core stack (a separate repo) referenced via repo_dir.
Purpose
Continuously maintain the running stack without an operator at the keyboard:
- Detect and remediate service outages.
- Periodically check for and apply container upgrades (with test + rollback).
- Communicate with the operator over Matrix (chat.semprini.me), including asking questions when an upgrade needs a human decision.
It is the unattended driver for the canonical stack-support agent
(agents/stack-support.agent.md) — it does not re-implement that logic, it
feeds the agent state and relays its results.
Key decisions
- Scope = Uptime Kuma's live monitor list. Read every health cycle, never
hard-coded. Anything Kuma tracks is in scope — including services owned by
other projects (e.g.
semprini-blog) that register Kuma monitors. This keeps the maintained set and the monitored set identical by construction. - Health signal = Kuma heartbeat. The daemon reads Kuma's own up/down result rather than re-probing, reusing the stack's existing monitoring.
- Kuma data access via a throwaway
keinos/sqlite3container mounting theuptime-kuma-datavolume read-only (the daemon user has Docker but not host access to root-owned volume files). - Runner = Claude Code headless (
claude -p), withagents/stack-support.agent.mdinjected as the system prompt. The agent returns a JSON envelope (status/summary/question/options) the daemon acts on. - Runs as
paul(in thedockergroup), not root — Claude Code refusesbypassPermissionsas root. - Autonomy = full by default: remediate and apply safe upgrades; escalate
only major/breaking upgrades to Matrix and resume on reply. Configurable to
fix-onlyornotify. - Matrix transport: dedicated bot
@maintainer:semprini.me(created via the Synapse registration shared secret) in a private room with@paul:semprini.me. - Code changes go through pull requests. When the agent edits tracked files
it branches, pushes, and opens a PR on Gitea (
git.semprini.me) as theclaude-codeuser rather than committing tomain, giving the operator a review gate. Live remediation (restart/redeploy) is not gated — only the git change is. claude-code is an OIDC-linked, push-capable collaborator; since git.semprini.me disables password login + HTTP basic auth,register-gitea-bot.shmints a personal access token via the gitea admin CLI inside theuser-giteacontainer (mirroring the Matrix bot's shared-secret pattern). A PAT still works for git-over-HTTPS and the API, so no SSH exposure or core-stack change is required. The daemon injects the bot's git identity + credential (GIT_CONFIG_*, scoped to the Gitea host so it overrides the repo's ownuser.*) and the API coordinates into the headless agent; the agent opens the PR itself.
Cadence
| Loop | Default interval | Config key |
|---|---|---|
| Health check + remediation | 5 min | health_interval_seconds |
| Upgrade review | 7 days | upgrade_interval_seconds |
| Matrix reply polling | 30 s | matrix_poll_seconds |
| Quiet health heartbeat | daily | heartbeat_interval_seconds |
Escalation flow
- Agent returns
status=escalatewith aquestion+options. - Daemon posts a numbered question to Matrix and pauses upgrade cycles.
- Admin replies (a number or free text).
- Daemon feeds the decision back to the agent, which carries it out and reports.
Operational notes
- The bot DM room invite must be accepted once by
@paulin Element. - Secrets (
config.json,.bot-secrets) are gitignored. - A single unanswered escalation blocks new upgrade cycles (health remediation continues regardless).