By request: one agent in Matrix, not several. Removes matrixBot.js, router.js (chat-vs-code-task classifier), litellm.js (claude-agent's own LiteLLM client), the matrix-bot-sdk dependency, runChatTask() and its gitea.js branch/PR helpers (createBranch/createPullRequest — only ever called from the now-removed chat flow), and every Matrix/LiteLLM env var from claude-agent's compose service. claude-agent already left the control room manually before this merge. It keeps its Gitea-webhook-triggered PR review, which never touched Matrix or LiteLLM to begin with. Makes PR #12 (the claude-bot/Hermes cross-reply cascade fix) moot — the bug can't happen once claude-agent has no Matrix client at all. Close #12 without merging once this lands.
146 lines
8.0 KiB
Markdown
146 lines
8.0 KiB
Markdown
# gitops-automation
|
|
|
|
Claude Code + Hermes automation wired into Gitea + Portainer + Matrix on this VPS. See
|
|
`~/.claude/plans/cozy-honking-lantern.md` on the host for the original design rationale
|
|
(some of it — the Matrix chat bot on claude-agent — has since been superseded, see below).
|
|
|
|
What this gives you today:
|
|
- **PR review**: opening/updating a PR in a watched Gitea repo gets a Claude-authored
|
|
review comment (`claude-agent`, triggered by a Gitea webhook — nothing to do with Matrix).
|
|
- **GitOps redeploy**: pushing to `main` on this repo rebuilds the `claude-agent` image
|
|
(Gitea Actions) and redeploys the stack, chained as the last step of that same workflow.
|
|
- **Matrix**: **Hermes is the only agent present in Matrix.** `claude-agent` used to also
|
|
run a Matrix bot (`!claude`/`!ai` commands, then no-prefix auto-routing) — that's been
|
|
removed entirely (by request: one agent in Matrix, not several). Hermes has its own
|
|
native Matrix connection, responds to `@hermes <message>` in shared rooms (no mention
|
|
needed in DMs), and has its own tools (terminal, code execution, web search, etc.) — see
|
|
its docs at https://hermes-agent.nousresearch.com for what it can do. It does not (yet)
|
|
have the old branch/PR-opening workflow the Matrix bot used to have; that logic still
|
|
exists in git history if it's worth reviving as a Hermes tool/skill later.
|
|
|
|
Nothing here auto-merges PRs. Every path stops at a comment or an open PR — a human clicks
|
|
merge (enforced by branch protection on `main`, not just by convention — see below).
|
|
|
|
## Prerequisites (one-time, on the VPS)
|
|
|
|
```bash
|
|
sudo usermod -aG docker william # then start a new shell/session
|
|
docker network create web
|
|
```
|
|
|
|
## Bring-up order
|
|
|
|
1. **DNS** — confirm these resolve to `217.160.66.143`:
|
|
`gitea.apps.williamturner.eu`, `portainer.apps.williamturner.eu`,
|
|
`matrix.apps.williamturner.eu`, `agent.apps.williamturner.eu`.
|
|
|
|
2. **Front Gitea with Traefik**: edit `~/gitea/docker-compose.yml` — add it to the `web`
|
|
network and Traefik labels (mirror the `matrix-homeserver` block in this repo's
|
|
`docker-compose.yml`, using port `3000` as the service port and rule
|
|
`` Host(`gitea.apps.williamturner.eu`) ``). Update `~/gitea/data/gitea/conf/app.ini`:
|
|
`DOMAIN` and `ROOT_URL` → `https://gitea.apps.williamturner.eu/`. Recreate the container,
|
|
confirm the HTTPS URL works, *then* remove `3000:3000` from the port mapping.
|
|
|
|
3. **Front Portainer with Traefik**: same pattern on `~/portainer-compose.yaml`, service
|
|
port `9443` (Portainer serves TLS itself on that port — either terminate TLS at Traefik
|
|
with `traefik.http.services.portainer.loadbalancer.server.scheme=https` and
|
|
`serversTransport` with insecure skip-verify, or simplest: also expose Portainer's plain
|
|
HTTP port internally and point Traefik at that instead). Confirm
|
|
`https://portainer.apps.williamturner.eu` works before removing `9443:9443`.
|
|
|
|
4. **Create the `gitops-automation` repo in Gitea** (via `http://217.160.66.143:3000` if
|
|
step 2 isn't done yet, otherwise the HTTPS URL), push this directory to it.
|
|
|
|
5. **Gitea API token**: user Settings → Applications → generate a token with repo +
|
|
webhook scopes. Put it in `.env` as `GITEA_TOKEN`.
|
|
|
|
6. **Gitea Actions runner**: admin Settings → Actions → Runners → create registration
|
|
token → `.env` as `ACT_RUNNER_REGISTRATION_TOKEN`. Also set repo-level Actions
|
|
variables `GITEA_HOST` (e.g. `gitea.apps.williamturner.eu`) and secret `GITEA_TOKEN`
|
|
(Settings → Actions → Variables/Secrets on the repo) — the workflow in
|
|
`.gitea/workflows/build.yml` reads those.
|
|
|
|
7. **First image build** (registry is empty until Actions runs once):
|
|
```bash
|
|
cp .env.example .env # fill in values as you go
|
|
docker build -t "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)" ./agent
|
|
docker login <gitea-host> -u <your-username>
|
|
docker push "$(grep GITEA_REGISTRY_IMAGE .env | cut -d= -f2)"
|
|
```
|
|
|
|
8. **Bring up the stack**, ideally as a Portainer "Repository" stack pointed at this repo
|
|
(so it's also the GitOps redeploy target) — or directly:
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
9. **First boot: Matrix bot account** — with `MATRIX_ALLOW_REGISTRATION=true` in `.env`,
|
|
redeploy `matrix-homeserver`, then register the bot:
|
|
```bash
|
|
curl -s https://matrix.apps.williamturner.eu/_matrix/client/v3/register \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"username":"claude-bot","password":"<pick one>","auth":{"type":"m.login.dummy"}}'
|
|
```
|
|
This returns an `access_token` — put it in `.env` as `MATRIX_BOT_TOKEN`. Then set
|
|
`MATRIX_ALLOW_REGISTRATION=false` and redeploy `matrix-homeserver` again.
|
|
|
|
10. **Control room**: from any Matrix client logged in as yourself on this homeserver,
|
|
create a room, invite `@claude-bot:matrix.apps.williamturner.eu`, copy the room ID
|
|
into `.env` as `MATRIX_CONTROL_ROOM_ID`. Redeploy `claude-agent`.
|
|
|
|
11. **Portainer stack webhook**: in the stack's settings, enable the webhook, copy the URL
|
|
into `.env`/repo secrets as `PORTAINER_STACK_WEBHOOK_URL`.
|
|
|
|
12. **Gitea webhooks** on each repo you want automation for:
|
|
- push → `main` → `PORTAINER_STACK_WEBHOOK_URL` (only needed on *this* repo, for
|
|
GitOps redeploy of the automation stack itself)
|
|
- pull request (opened, synchronized) → `https://agent.apps.williamturner.eu/webhooks/gitea`,
|
|
secret = `GITEA_WEBHOOK_SECRET`, on every repo you want auto-reviewed.
|
|
|
|
13. **Firewall**: `sudo ufw allow 80/tcp 443/tcp`; once the HTTPS routes above are all
|
|
confirmed working, `sudo ufw delete allow 3000/tcp` and `sudo ufw delete allow 9443/tcp`.
|
|
|
|
14. **Branch protection on `main`**, on every repo you want the bot working on (so it's
|
|
structurally limited to opening PRs, never merging or pushing directly) — via the repo's
|
|
Settings → Branches → Add Rule, or the API:
|
|
```bash
|
|
curl -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
|
|
"$GITEA_URL/api/v1/repos/<owner>/<repo>/branch_protections" \
|
|
-d '{
|
|
"rule_name": "main",
|
|
"enable_push": false,
|
|
"enable_merge_whitelist": true,
|
|
"merge_whitelist_usernames": ["<your-human-username>"]
|
|
}'
|
|
```
|
|
This blocks *all* direct pushes to `main` (everyone goes through a PR) and restricts
|
|
merging to the human usernames listed — `claude-bot` (see below) is never in that list,
|
|
so it structurally cannot merge, only open PRs, regardless of what its token can do.
|
|
|
|
15. **Dedicated bot account** (recommended over using your own account/token for the
|
|
agent): create a separate Gitea user (e.g. `claude-bot`) via Site Administration → User
|
|
Accounts, generate its own token (scopes: repository, issue, package — no `admin`,
|
|
`organization`, or `user` needed), add it as a repo Collaborator with **Write**
|
|
permission on each automated repo, then use its token as `GITEA_TOKEN` (agent) and
|
|
`REGISTRY_TOKEN` (CI secret) instead of your own. This makes every PR comment, branch,
|
|
and PR clearly attributed to the bot instead of you, and its access is easy to revoke
|
|
independently.
|
|
|
|
## Smoke test
|
|
|
|
- Open a throwaway PR on a repo with the PR webhook set → expect a Claude review comment.
|
|
- In the Matrix control room: `@hermes hello` → expect a reply from Hermes.
|
|
- `git push` to `main` on this repo (touching `agent/**`) → expect a Gitea Actions run,
|
|
then a chained Portainer redeploy at the end of that same workflow.
|
|
|
|
## Notes
|
|
|
|
- `agent/src/runner.js` only ever runs read-only `git clone`/`fetch`/`checkout` for the PR
|
|
diff it reviews — Claude Code itself is denied `Edit`/`Write`/commit/push tools
|
|
(`--disallowedTools`), so this path can never modify a repo, only comment on it.
|
|
- `.gitea/workflows/build.yml` assumes the act_runner label `docker` — check
|
|
`GITEA_RUNNER_LABELS` in `docker-compose.yml` matches what you actually registered.
|
|
- Hermes's own config/memory/skills live in `/home/william/hermes-data` on the host
|
|
(bind-mounted, not in this repo) — back that up separately if it accumulates anything
|
|
worth keeping.
|