Initial version of the Hermes self learning agent setup of JR IT Services.

This commit is contained in:
2026-04-21 11:53:29 +02:00
commit 7f9b0e6c5f
31 changed files with 1966 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.{sh,bash}]
indent_style = space
indent_size = 2
[*.yml]
indent_style = space
indent_size = 2
[*.yaml]
indent_style = space
indent_size = 2
[Dockerfile]
indent_style = tab
+56
View File
@@ -0,0 +1,56 @@
# Copy to .env and adapt.
# --- project naming ---
COMPOSE_PROJECT_NAME=alan
# --- hermes image ---
HERMES_IMAGE=nousresearch/hermes-agent
HERMES_TAG=stable
# --- host paths ---
HERMES_DATA_DIR=./state/hermes
HERMES_BACKUP_DIR=./backups/hermes
# --- host ports ---
# Bound to 127.0.0.1 on purpose.
# Hermes reaches the internet via the external br0-backed Docker network,
# while the UI/API remain local to the workstation unless you add a reverse proxy.
HERMES_API_PORT=18642
HERMES_DASHBOARD_PORT=19119
# --- resources ---
HERMES_MEMORY_LIMIT=4g
HERMES_CPUS=2.0
HERMES_SHM_SIZE=1g
DASHBOARD_MEMORY_LIMIT=512m
DASHBOARD_CPUS=0.5
# --- agent settings ---
TZ=Europe/Berlin
PUID=1000
PGID=1000
# --- docker networks ---
# Shared service-to-service network used only when explicitly needed.
AGENTS_SHARED_NETWORK=alan_agents_shared
# External Docker network that must already exist and be backed by your host bridge br0.
# Recommended driver: macvlan with parent=br0.
BR0_DOCKER_NETWORK=br0_lan
# --- helper values for scripts/docs only ---
# Used by scripts/alanctl net-create-br0. Adjust to your LAN.
BR0_PARENT=br0
BR0_SUBNET=192.168.10.0/24
BR0_GATEWAY=192.168.10.1
BR0_IP_RANGE=192.168.10.224/28
# Optional host-side shim interface for host<->container reachability on macvlan.
BR0_HOST_SHIM_CIDR=192.168.10.239/32
# Optional: forward provider creds from host env instead of storing only inside state/hermes/.env.
# OPENAI_API_KEY=
# ANTHROPIC_API_KEY=
# OPENROUTER_API_KEY=
# Tipp: fuer reproduzierbare Updates lieber einen festen Release-Tag setzen
# statt dauerhaft latest zu verwenden. Beispiel:
# HERMES_TAG=v0.9.3
+35
View File
@@ -0,0 +1,35 @@
name: Bug Report
description: Fehler im Repo, Skript oder Compose-Verhalten melden
title: "bug: "
labels:
- bug
body:
- type: textarea
id: summary
attributes:
label: Zusammenfassung
description: Was ist kaputt?
validations:
required: true
- type: textarea
id: environment
attributes:
label: Umgebung
description: Arch-Version, Docker-Version, Repo-Tag, Hermes-Tag
- type: textarea
id: steps
attributes:
label: Schritte zur Reproduktion
description: Bitte die exakten Befehle angeben
- type: textarea
id: expected
attributes:
label: Erwartetes Verhalten
- type: textarea
id: actual
attributes:
label: Tatsaechliches Verhalten
- type: textarea
id: logs
attributes:
label: Relevante Logs
@@ -0,0 +1,24 @@
name: Feature Request
description: Erweiterung fuer Betrieb, Doku oder Workflow vorschlagen
title: "feat: "
labels:
- enhancement
body:
- type: textarea
id: problem
attributes:
label: Welches Problem loest die Aenderung?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Vorschlag
description: Beschreibe den gewuenschten Zielzustand
validations:
required: true
- type: textarea
id: impact
attributes:
label: Betriebswirkung
description: Hat die Aenderung Folgen fuer Update, Backup, Ports, Netze oder `.env`?
+18
View File
@@ -0,0 +1,18 @@
## Zusammenfassung
-
## Checkliste
- [ ] `CHANGELOG.md` aktualisiert
- [ ] passende Doku aktualisiert
- [ ] `./scripts/release-check` ausgefuehrt
- [ ] `./scripts/alanctl validate` ausgefuehrt
- [ ] bei Upgrade-Auswirkung `docs/upgrade-policy.md` geprueft
## Betriebswirkung
- [ ] keine
- [ ] `.env.example` geaendert
- [ ] Ports/Netze/Volumes geaendert
- [ ] Update-/Rollback-Verhalten geaendert
+36
View File
@@ -0,0 +1,36 @@
name: repo-check
on:
push:
branches:
- main
- 'feat/**'
- 'fix/**'
- 'docs/**'
- 'chore/**'
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Basic file checks
run: |
test -f README.md
test -f CHANGELOG.md
test -f compose.yaml
test -x scripts/alanctl
test -x scripts/release-check
- name: Shell syntax
run: |
bash -n scripts/alanctl
bash -n scripts/release-check
bash -n scripts/new-release
- name: Compose validation
run: |
docker compose -f compose.yaml config >/dev/null
+8
View File
@@ -0,0 +1,8 @@
.env
.env.local
backups/
state/
third_party/
*.log
*.tar.gz
.DS_Store
+27
View File
@@ -0,0 +1,27 @@
# Changelog
Alle relevanten Aenderungen an diesem Repo werden hier dokumentiert.
Das Format orientiert sich an "Keep a Changelog" und nutzt semantische Versionen fuer Repo-Releases.
## [Unreleased]
### Added
- Platzhalter fuer neue Features
### Changed
- Platzhalter fuer geaenderte Betriebslogik
### Fixed
- Platzhalter fuer Bugfixes
### Security
- Platzhalter fuer sicherheitsrelevante Anpassungen
## [0.1.0] - 2026-04-19
### Added
- Erstes Workstation-Setup fuer Hermes parallel zu OpenClaw
- br0-faeiges Docker-Layout mit externem Macvlan-Uplink
- Backup-, Update- und Rollback-Workflow ueber `alanctl`
- Gitea-Dokumentation und Maintainer-Struktur
+20
View File
@@ -0,0 +1,20 @@
# Default owners for the whole repository.
# Gitea installations with CODEOWNERS support can use this file for review hints.
# If your Gitea instance does not enforce CODEOWNERS automatically,
# use docs/maintainers.md as the operational fallback.
* @johannes
# Core runtime and deployment logic
/compose.yaml @johannes
/compose/ @johannes
/scripts/ @johannes
/systemd/ @johannes
# Documentation and process
/docs/ @johannes
/README.md @johannes
/CHANGELOG.md @johannes
/CONTRIBUTING.md @johannes
/SECURITY.md @johannes
/.gitea/ @johannes
+50
View File
@@ -0,0 +1,50 @@
# Contributing
Dieses Repo ist fuer eine Workstation-/Homelab-nahe Infrastruktur gedacht. Aenderungen sollten daher vor allem diese Ziele schuetzen:
- reproduzierbares Verhalten
- klare Trennung zwischen Hermes und OpenClaw
- sichere Defaults
- moeglichst wenig manuelle Nacharbeit nach Updates
## Branching
- `main` bleibt deploybar
- neue Arbeit in `feat/...`, `fix/...`, `docs/...`, `chore/...`
- Merge erst nach lokalem Check
## Vor jedem Merge
```bash
./scripts/release-check
./scripts/alanctl validate
./scripts/alanctl doctor
```
## Formatierung und Ownership
- nutze einen Editor mit `.editorconfig`-Unterstuetzung
- beachte `CODEOWNERS` und `docs/maintainers.md` bei Review und Zustaendigkeit
- Aenderungen an Runtime-, Netz- oder Update-Logik nicht ohne bewusste Review freigeben
## Commit-Stil
Empfohlen:
- `feat: ...`
- `fix: ...`
- `docs: ...`
- `chore: ...`
- `refactor: ...`
## Release-Regel
Jede aenderungsrelevante Anpassung an Compose, Skripten oder Netz-Logik braucht:
- Eintrag in `CHANGELOG.md`
- kurzen Hinweis in passender Doku
- bei Upgrade-Auswirkung einen Eintrag in `docs/upgrade-policy.md`
## Security-Meldungen
Bitte keine sicherheitsrelevanten Probleme oeffentlich als normales Issue posten. Nutze dafuer die Hinweise in `SECURITY.md`.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Johannes Rest
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
SHELL := /usr/bin/env bash
.PHONY: bootstrap up down restart status logs doctor init-hermes backup update shell dashboard
bootstrap:
./scripts/alanctl bootstrap
up:
./scripts/alanctl up
down:
./scripts/alanctl down
restart:
./scripts/alanctl restart
status:
./scripts/alanctl status
logs:
./scripts/alanctl logs
doctor:
./scripts/alanctl doctor
init-hermes:
./scripts/alanctl init-hermes
backup:
./scripts/alanctl backup
update:
./scripts/alanctl update-hermes
shell:
./scripts/alanctl shell
dashboard:
./scripts/alanctl up --dashboard
+228
View File
@@ -0,0 +1,228 @@
# alan-agents-workstation
Docker-Setup fuer eine Arch-Linux-Workstation, damit **Hermes Agent** parallel zu **OpenClaw** laufen kann, ohne sich in die Quere zu kommen.
Wichtige Netzannahme in diesem Repo:
- **Internetzugang fuer Hermes laeuft ueber dein `br0`**.
- Dafuer haengt Hermes an einem **externen Docker-Macvlan-Netz**, das auf dem Host-Bridge-Interface `br0` basiert.
- OpenClaw bleibt ein **separater Stack** mit eigenen Ports, Volumes und idealerweise demselben Uplink-Prinzip.
Der Fokus liegt auf:
- saubere Trennung von Ports, Volumes und Netzwerken
- resilientes Compose-Setup
- komfortabler Workflow ueber `alanctl`
- einfache Ablage in einem Gitea-Repo
- explizite Beruecksichtigung von `br0`
## Was dieses Repo liefert
- `compose.yaml` fuer Hermes Gateway und optionales Dashboard
- Wrapper-Skript `scripts/alanctl`
- Backup/Restore/Update/Rollback-Workflow
- Doku fuer Architektur, `br0`, Betrieb und Gitea-Import
- systemd-User-Unit als Vorlage
- Beispiel fuer getrennte OpenClaw-Integration
## Schnellstart
```bash
cp .env.example .env
# .env an dein LAN anpassen: BR0_SUBNET, BR0_GATEWAY, BR0_IP_RANGE
./scripts/alanctl bootstrap
./scripts/alanctl net-create-br0
./scripts/alanctl init-hermes
./scripts/alanctl up
```
Optional mit Dashboard:
```bash
./scripts/alanctl up --dashboard
```
Danach:
- Hermes Health/API lokal: `http://127.0.0.1:18642/health`
- Hermes Dashboard lokal: `http://127.0.0.1:19119`
## Langfristige Repo-Pflege in Gitea
Neu fuer den laufenden Betrieb:
- `CHANGELOG.md` fuer nachvollziehbare Aenderungen
- `CONTRIBUTING.md` fuer Arbeitsregeln
- `docs/release-process.md` fuer Tags und Release Notes
- `docs/release-template.md` als kopierbare Vorlage fuer Gitea-Releases
- `docs/upgrade-policy.md` fuer Upgrade-Klassen und Rollback-Regeln
- `docs/maintenance.md` fuer wiederkehrende Pflege
- `.gitea/ISSUE_TEMPLATE/*` und `.gitea/pull_request_template.md`
- `.gitea/workflows/repo-check.yml` fuer einfache Repo-Pruefungen
- `scripts/release-check` und `scripts/new-release`
- `.editorconfig` fuer konsistente Formatierung
- `CODEOWNERS` und `docs/maintainers.md` fuer Review-Zustaendigkeiten
Release-Vorbereitung:
```bash
./scripts/release-check
./scripts/new-release 0.1.1
```
## Warum zwei Netzebenen?
- **`br0_uplink`**: externer Uplink fuer Internet/LAN via `br0`
- **`hermes_private`**: internes Compose-Netz fuer saubere Service-Kommunikation
- **`alan_agents_shared`**: optionales Querverbindungsnetz nur bei echtem Bedarf
So bekommt Hermes den Internetzugang ueber `br0`, waehrend API und Dashboard lokal auf `127.0.0.1` gebunden bleiben.
## Workflow
### Status
```bash
./scripts/alanctl status
```
### Logs
```bash
./scripts/alanctl logs
./scripts/alanctl logs dashboard
```
### Health / Diagnose
```bash
./scripts/alanctl doctor
```
### Backup
```bash
./scripts/alanctl backup
```
### Restore
```bash
./scripts/alanctl restore ./backups/hermes/hermes-YYYYMMDD-HHMMSS.tar.gz
```
### Update
Empfohlen: Hermes **nicht** auf `latest` lassen, sondern auf einen bewusst gesetzten Tag pinnen.
```bash
./scripts/alanctl pin-hermes stable
./scripts/alanctl update-hermes # aktualisiert auf den aktuell gepinnten Tag
./scripts/alanctl update-hermes v0.9.3 # gezieltes Upgrade auf einen bestimmten Tag
```
Rollback bei Bedarf:
```bash
./scripts/alanctl rollback-hermes
```
## Arch-Linux-Hinweise
Installiere mindestens:
```bash
sudo pacman -S docker docker-compose curl iproute2
sudo systemctl enable --now docker
sudo usermod -aG docker "$USER"
newgrp docker
```
Fuer den optionalen Host-Shim brauchst du `sudo` fuer `ip link` und `ip route`.
## Wichtige Macvlan-Einschraenkung
Bei Docker-Macvlan koennen **Host und Container nicht direkt miteinander sprechen**, solange du keinen Host-Shim anlegst.
Wenn du das brauchst, z. B. fuer lokale Tests vom Host auf die `br0`-IP des Containers:
```bash
./scripts/alanctl net-create-shim
```
Details dazu stehen in `docs/bridge-br0.md`.
## Verzeichnisstruktur
```text
.
├── compose
│ └── openclaw.override.example.yaml
├── compose.yaml
├── docs
│ ├── architecture.md
│ ├── bridge-br0.md
│ ├── gitea-import.md
│ ├── hermes.md
│ ├── maintenance.md
│ ├── openclaw-coexistence.md
│ ├── release-process.md
│ └── upgrade-policy.md
├── scripts
│ ├── alanctl
│ ├── new-release
│ └── release-check
├── state
│ └── hermes/
├── backups
│ └── hermes/
└── systemd
└── user
└── alan-hermes.service
```
## Warum die Ports absichtlich ungewoehnlich sind
Die offiziellen Hermes-Beispiele verwenden hostseitig meist `8642` fuer die API und `9119` fuer das Dashboard. Dieses Repo mappt absichtlich auf `18642` und `19119`, bindet sie aber nur an `127.0.0.1`, damit ein bestehendes OpenClaw- oder sonstiges Dev-Setup auf der Workstation seltener kollidiert.
## Maintainer und Review
- `.editorconfig` sorgt fuer konsistente Grundformatierung in Editor und CI
- `CODEOWNERS` beschreibt Default-Owner und Review-Schwerpunkte
- `docs/maintainers.md` ist die betriebliche Fallback-Doku, falls deine Gitea-Instanz `CODEOWNERS` nicht automatisch auswertet
## Lizenz und Security
- `LICENSE`: MIT-Lizenz fuer klare Weitergabe und Anpassung
- `SECURITY.md`: Richtlinie fuer Security-Meldungen und gepflegte Staende
## Sicherheit und Betriebsgrenzen
- Dieses Repo trennt Hermes und OpenClaw bewusst.
- Nutze das Shared-Netz nur, wenn wirklich noetig.
- Lege keine Secrets ins Git-Repo.
- Wenn du OpenClaw separat betreibst, gib ihm andere Host-Ports und eigene Volumes.
- Das `br0`-Uplink-Netz ist **extern** und wird absichtlich nicht blind bei `docker compose up` erzeugt.
## Naechster sinnvoller Ausbau
- Reverse-Proxy mit lokalem DNS-Namen
- systemd-User-Integration aktivieren
- Borg/Restic fuer Off-Host-Backups
- eigene Profiles fuer mehrere Hermes-Instanzen
- optional identisches `br0`-Uplink-Muster fuer OpenClaw
## Update-Strategie
Dieses Repo ist bewusst so gebaut, dass Updates kontrolliert ablaufen:
- `HERMES_TAG` wird in `.env` gepinnt
- `update-hermes` erstellt zuerst ein Backup
- danach wird das Ziel-Image gepullt und Hermes neu erstellt
- der Healthcheck wird aktiv abgewartet
- bei Fehlschlag wird automatisch auf den vorherigen Tag zurueckgestellt
- der zuletzt erfolgreiche Stand wird in `state/hermes/.last-known-good.env` gespeichert
Fuer reproduzierbare Setups auf Workstation oder Homeserver ist das deutlich robuster als blindes Arbeiten mit `latest`.
+65
View File
@@ -0,0 +1,65 @@
# Security Policy
## Scope
Dieses Repo beschreibt und automatisiert ein lokales Infrastruktur-Setup fuer eine Arch-Linux-Workstation.
Sicherheitsrelevante Meldungen sind besonders wichtig bei:
- Container-Privilegien und Volume-Mounts
- Netzwerkdesign rund um `br0`, `macvlan` und lokale Bindings
- Backup-/Restore- und Rollback-Verhalten
- Secret-Handling in `.env`, systemd-Units oder Hilfsskripten
- Healthchecks, Update-Logik und unerwarteten Downgrade-/Rollback-Faellen
## Was als Sicherheitsproblem gilt
Bitte melde insbesondere:
- unbeabsichtigte Exponierung von Hermes oder OpenClaw auf allen Interfaces
- unsichere Defaults bei Docker-Rechten oder Netzfreigaben
- Fehler, durch die Secrets im Repo, in Logs oder in Backups landen koennen
- Rollback-/Restore-Pfade, die zu Datenverlust oder Privilegienproblemen fuehren
- Konfigurationsfehler, die Container versehentlich zu stark koppeln
## Was nicht darunter faellt
Diese Punkte sind wichtig, aber keine Security-Meldung im engeren Sinn:
- reine Doku-Fehler ohne Sicherheitsauswirkung
- Feature-Wuensche
- Port- oder Naming-Praeferenzen ohne Risiko
- allgemeine Bedienfragen
## Meldung von Schwachstellen
Bitte **keine sicherheitsrelevanten Details direkt in einem oeffentlichen Issue posten**.
Empfohlener Ablauf:
1. Privaten Kontakt zum Maintainer herstellen
2. Betroffene Datei, Version/Tag und Konfiguration beschreiben
3. Moegliche Auswirkung erklaeren
4. Reproduktionsschritte knapp dokumentieren
5. Falls moeglich einen sicheren Fix-Vorschlag beilegen
Empfohlene Informationen:
- Repo-Tag oder Commit
- relevante `.env`-Werte **ohne Secrets**
- Host-Umgebung (z. B. Arch-Version, Docker-Version)
- Logs oder `doctor`-Ausgaben, soweit bereinigt
## Reaktionsziel
Es gibt hier kein formelles SLA. Ziel ist dennoch:
- zeitnahe Bestaetigung der Meldung
- Einordnung nach Risiko
- Fix oder Mitigation-Hinweis
- kurzer Eintrag im `CHANGELOG.md`, wenn eine harte Aenderung noetig ist
## Support-Fenster
Aktiv gepflegt wird jeweils der aktuelle Stand auf `main` sowie der letzte getaggte Release-Stand, soweit reproduzierbar.
Aeltere Tags koennen Hinweise bekommen, aber ohne zugesagte Rueckportierung.
+70
View File
@@ -0,0 +1,70 @@
services:
hermes:
image: ${HERMES_IMAGE:-nousresearch/hermes-agent}:${HERMES_TAG:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-alan}-hermes
restart: unless-stopped
command: gateway run
shm_size: ${HERMES_SHM_SIZE:-1g}
ports:
- "127.0.0.1:${HERMES_API_PORT:-18642}:8642"
environment:
TZ: ${TZ:-Europe/Berlin}
volumes:
- ${HERMES_DATA_DIR:-./state/hermes}:/opt/data
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8642/health', timeout=3).read()\""]
interval: 30s
timeout: 5s
retries: 5
start_period: 45s
mem_limit: ${HERMES_MEMORY_LIMIT:-4g}
cpus: ${HERMES_CPUS:-2.0}
security_opt:
- no-new-privileges:true
networks:
hermes_private:
agents_shared:
br0_uplink:
aliases:
- hermes
dashboard:
image: ${HERMES_IMAGE:-nousresearch/hermes-agent}:${HERMES_TAG:-latest}
container_name: ${COMPOSE_PROJECT_NAME:-alan}-hermes-dashboard
restart: unless-stopped
command: dashboard --host 0.0.0.0
profiles: ["dashboard"]
ports:
- "127.0.0.1:${HERMES_DASHBOARD_PORT:-19119}:9119"
environment:
TZ: ${TZ:-Europe/Berlin}
GATEWAY_HEALTH_URL: http://hermes:8642
volumes:
- ${HERMES_DATA_DIR:-./state/hermes}:/opt/data:ro
depends_on:
hermes:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:9119', timeout=3).read()\""]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s
mem_limit: ${DASHBOARD_MEMORY_LIMIT:-512m}
cpus: ${DASHBOARD_CPUS:-0.5}
security_opt:
- no-new-privileges:true
networks:
- hermes_private
networks:
hermes_private:
name: ${COMPOSE_PROJECT_NAME:-alan}_hermes_private
driver: bridge
internal: false
agents_shared:
name: ${AGENTS_SHARED_NETWORK:-alan_agents_shared}
external: true
br0_uplink:
name: ${BR0_DOCKER_NETWORK:-br0_lan}
external: true
+38
View File
@@ -0,0 +1,38 @@
# Example only.
# Use this as a pattern if you want OpenClaw managed in a separate repo or compose project.
# The important part is the same separation model as Hermes:
# - localhost-bound host ports
# - own state directory
# - own private network
# - optional shared network
# - external br0-backed uplink network for LAN/Internet
services:
openclaw:
image: ghcr.io/example/openclaw:latest
container_name: openclaw
restart: unless-stopped
ports:
- "127.0.0.1:18080:8080"
environment:
TZ: Europe/Berlin
volumes:
- ./state/openclaw:/app/data
security_opt:
- no-new-privileges:true
mem_limit: 4g
cpus: 2.0
networks:
- openclaw_private
- agents_shared
- br0_uplink
networks:
openclaw_private:
name: openclaw_private
driver: bridge
agents_shared:
name: alan_agents_shared
external: true
br0_uplink:
name: br0_lan
external: true
+63
View File
@@ -0,0 +1,63 @@
# Architektur
## Ziele
- Hermes parallel zu OpenClaw betreiben, ohne Port-, Volume- oder Netzwerk-Kollisionen.
- Reproduzierbares Setup fuer eine Arch-Linux-Workstation.
- Resilienter Betrieb mit Healthchecks, Restart-Policy, Backups und Update-Weg.
- Komfortabler Workflow ueber ein einziges Wrapper-Skript.
- Internetzugang fuer Hermes ueber die Host-Bridge `br0`.
## Trennungsprinzipien
### 1. Eigene Datenverzeichnisse
- Hermes: `./state/hermes`
- OpenClaw: separat, z. B. `./state/openclaw`
Hermes schreibt seine komplette Identitaet, Konfiguration, Skills, Sessions und Memory in das gemountete Datenverzeichnis. Das darf nicht mit einem zweiten Hermes-Gateway geteilt werden.
### 2. Eigene Host-Ports
Default in diesem Repo:
- Hermes API: `127.0.0.1:18642 -> 8642`
- Hermes Dashboard: `127.0.0.1:19119 -> 9119`
Die absichtlich ungewohnten Host-Ports reduzieren Kollisionen mit anderen lokalen Stacks. Dass die Bindung auf `127.0.0.1` erfolgt, ist Absicht: der LAN-Uplink laeuft separat ueber `br0`, die Bedienoberflaechen bleiben lokal.
### 3. Eigene Netzrollen
- `hermes_private`: internes Docker-Bridge-Netz fuer Compose-interne Kommunikation
- `br0_uplink`: externes Docker-Macvlan-Netz auf Parent `br0`
- `alan_agents_shared`: optionales Querverbindungsnetz fuer Hermes/OpenClaw
Damit ist die Verkehrsrichtung klar getrennt:
- Internet/LAN raus ueber `br0_uplink`
- interne Service-Aufloesung ueber `hermes_private`
- bewusste Interconnects ueber `alan_agents_shared`
### 4. Eigene Compose-Projektkennung
Der Standard-Projektname ist `alan`. Dadurch bleiben Container, Netzwerke und sonstige Docker-Ressourcen sauber benannt.
## Resilienz
- `restart: unless-stopped`
- HTTP-Healthchecks fuer Gateway und Dashboard
- `no-new-privileges`
- definierte Ressourcenlimits
- Backup- und Restore-Skripte
- idempotentes Bootstrap fuer `.env`, Verzeichnisse und Shared-Netz
- explizite Validierung des externen `br0`-Netzes vor dem Start
## Nicht absichtlich eingebaut
- kein Reverse-Proxy per Default
- kein gemeinsamer Docker-Socket fuer Hermes und OpenClaw
- kein wildes Mounten von `/home`
- kein direkter Zwang, OpenClaw in denselben Compose-Stack zu ziehen
- keine blind automatische Ermittlung deiner LAN-Parameter
Das ist Absicht: weniger Seiteneffekte, weniger Sicherheitsrisiko, bessere Fehlersuche.
+66
View File
@@ -0,0 +1,66 @@
# Betrieb ueber `br0`
## Warum hier Macvlan verwendet wird
Du hast vorgegeben, dass der Container **ueber deine Bridge `br0`** laufen soll, wenn Internetzugang benoetigt wird.
Fuer Docker auf Linux ist das sauberste Muster dafuer in der Regel ein **externes Macvlan-Netz** mit:
- Parent: `br0`
- eigenem IP-Bereich fuer Container
- optionaler statischer IP pro Service
## Was du in `.env` anpassen musst
Mindestens:
```dotenv
BR0_PARENT=br0
BR0_SUBNET=192.168.10.0/24
BR0_GATEWAY=192.168.10.1
BR0_IP_RANGE=192.168.10.224/28
BR0_DOCKER_NETWORK=br0_lan
```
Optional:
```dotenv
BR0_HOST_SHIM_CIDR=192.168.10.239/32
```
Nimm fuer `BR0_IP_RANGE` am besten einen kleinen Bereich, der **nicht** durch DHCP an normale Clients vergeben wird.
## Externes Docker-Netz anlegen
```bash
./scripts/alanctl net-create-br0
```
Das erzeugt ein Docker-Macvlan-Netz auf `br0`.
## Bekannte Einschraenkung: Host <-> Container
Bei Macvlan koennen Host und Container standardmaessig **nicht direkt miteinander sprechen**. Das ist kein Fehler in deinem Compose-Setup, sondern normales Macvlan-Verhalten.
### Loesung: Host-Shim anlegen
```bash
./scripts/alanctl net-create-shim
```
Das erzeugt auf dem Host ein zusaetzliches Macvlan-Interface und routet den konfigurierten IP-Bereich dorthin.
## Wann du den Shim wirklich brauchst
- du willst den Container auf seiner `br0`-IP direkt vom Host anpingen
- du willst vom Host Dienste auf der `br0`-IP ansprechen
Wenn dir dagegen die lokal publizierten Ports `127.0.0.1:18642` und `127.0.0.1:19119` reichen, brauchst du den Shim oft gar nicht.
## Empfehlung fuer parallelen OpenClaw-Betrieb
- gleicher Parent `br0`
- anderer Host-Port
- anderer Volume-Pfad
- eigene statische IP oder eigene Zuteilung aus demselben `BR0_IP_RANGE`
- kein gemeinsames wildes Mounten von Host-Pfaden
+84
View File
@@ -0,0 +1,84 @@
# Gitea-Import
## Repo-Name
Empfohlen: `alan-agents-workstation`
## Schnellweg
```bash
cd /pfad/zu/alan
git init
git branch -M main
git add .
git commit -m "Initial workstation stack for Hermes + OpenClaw coexistence"
git remote add origin ssh://git@<dein-gitea-host>/<dein-user>/alan-agents-workstation.git
git push -u origin main
```
## Branching
Einfach und robust fuer Single-Workstation-Infra:
- `main` fuer den stabilen Stand
- `feat/...` fuer lokale Erweiterungen
- Tags fuer getestete Snapshots, z. B. `v0.1.0`
## Was ins Repo gehoert
- Compose-Dateien
- Skripte
- Doku
- Beispiel-Env-Dateien
- systemd-User-Units
## Was nicht ins Repo gehoert
- echte API-Keys
- echte Hermes-Daten aus `state/hermes`
- Backup-Archive
- lokale Debug-Logs
## Empfohlene Zusatzdateien fuer Gitea
Dieses Repo bringt bereits mit:
- `CHANGELOG.md`
- `CONTRIBUTING.md`
- `.gitea/ISSUE_TEMPLATE/*`
- `.gitea/pull_request_template.md`
- `.gitea/workflows/repo-check.yml`
- `docs/release-process.md`
- `docs/upgrade-policy.md`
Damit bekommst du in Gitea:
- strukturierte Fehlermeldungen und Feature-Wuensche
- einheitliche PR-Checklisten
- einfache serverseitige Grundpruefungen fuer das Repo
- nachvollziehbare Releases mit Tagging und Upgrade-Hinweisen
## Release-Empfehlung
Fuer jedes verteilte Repo-Update:
1. `CHANGELOG.md` pflegen
2. `./scripts/release-check` ausfuehren
3. Release-Tag setzen, z. B. `v0.1.1`
4. in Gitea Release Notes mit Upgrade-Hinweisen anlegen
## Beispiel fuer Release Notes
```text
Neu:
- robusteres br0-Uplink-Handling
- verbesserter Rollback-Workflow
Wichtig fuer bestehende Installationen:
- `.env.example` geaendert: neuer Key XYZ
- vor Upgrade bitte `./scripts/alanctl backup` ausfuehren
- Rollback bleibt ueber `./scripts/alanctl rollback-hermes` moeglich
```
+57
View File
@@ -0,0 +1,57 @@
# Hermes in diesem Setup
## Betriebsmodus
Dieses Repo faehrt Hermes **im Container** als Gateway.
Wichtige Daten landen in `./state/hermes`, das auf `/opt/data` im Container gemountet wird. Laut offizieller Doku ist `/opt/data` der Single Source of Truth fuer Config, Sessions, Skills, Memories und Logs.
## Initialisierung
1. `./scripts/alanctl bootstrap`
2. `./scripts/alanctl init-hermes`
3. `./scripts/alanctl up`
`init-hermes` startet den offiziellen Setup-Wizard interaktiv.
## Dashboard
Optional:
```bash
./scripts/alanctl up --dashboard
```
Danach ist das Dashboard standardmaessig auf `http://127.0.0.1:19119` erreichbar.
## Wichtige Hinweise
- Nicht zwei Hermes-Gateway-Container gegen dasselbe Datenverzeichnis starten.
- Falls du spaeter Messaging-Plattformen oder Provider-Keys nutzt, verwalte sie im Hermes-Datenordner bzw. in dessen `.env`.
- Fuer Browser-Tools ist `shm_size=1g` gesetzt.
## Update-Betrieb
Empfohlener Alltag:
```bash
./scripts/alanctl pin-hermes stable
./scripts/alanctl update-hermes
```
Gezieltes Upgrade auf einen bestimmten Tag:
```bash
./scripts/alanctl update-hermes v0.9.3
```
Der Update-Workflow macht automatisch:
1. Backup des Hermes-State
2. Pull des Ziel-Images
3. Recreate des Containers
4. Warten auf erfolgreichen Healthcheck
5. automatisches Zurueckstellen auf den vorherigen Tag bei Fehlern
Der zuletzt erfolgreiche Stand wird lokal gespeichert und kann mit `rollback-hermes` erneut ausgerollt werden.
+47
View File
@@ -0,0 +1,47 @@
# Maintainers and Review Ownership
Dieses Repo ist so klein gehalten, dass eine schlanke Maintainer-Struktur sinnvoller ist als komplexe Freigaberegeln.
## Primaerer Maintainer
- `@johannes`
## Verantwortungsbereiche
- **Runtime / Compose / Netzlogik**
- `compose.yaml`
- `compose/`
- `scripts/`
- `systemd/`
- **Dokumentation / Betriebswissen**
- `docs/`
- `README.md`
- `CHANGELOG.md`
- `CONTRIBUTING.md`
- `SECURITY.md`
- **Repository-Prozess in Gitea**
- `.gitea/`
- Release-Notizen
- Issue- und PR-Triage
## Review-Regeln
Empfohlene Minimalregeln:
- Aenderungen an `compose.yaml`, `scripts/alanctl` oder Netz-Setup immer bewusst reviewen.
- Vor Merge von Infrastruktur-Aenderungen mindestens `./scripts/release-check` ausfuehren.
- Bei Aenderungen mit Betriebsfolgen zusaetzlich `./scripts/alanctl validate` und `./scripts/alanctl doctor` laufen lassen.
- Bei Update- oder Rollback-Aenderungen immer `CHANGELOG.md` und passende Doku mitziehen.
## CODEOWNERS-Hinweis
Dieses Repo enthaelt eine `CODEOWNERS`-Datei fuer Instanzen, die das unterstuetzen.
Falls deine Gitea-Version oder Konfiguration CODEOWNERS nicht automatisch auswertet, nutze diese Datei als dokumentierte Zustaendigkeitsmatrix und setze die Review-Regel organisatorisch ueber Pull Requests um.
## Praktischer Betrieb in kleinen Teams
Falls spaeter weitere Maintainer dazukommen, ist dieses Muster sinnvoll:
- `@johannes` als primaerer Owner fuer Runtime und Releases
- ein zweiter Owner fuer Dokumentation und Review
- sicherheitsrelevante Aenderungen nie ohne sichtbare Review-Notiz mergen
+38
View File
@@ -0,0 +1,38 @@
# Maintenance
## Regelmaessige Aufgaben
Woechentlich oder vor groesseren Aenderungen:
```bash
./scripts/alanctl status
./scripts/alanctl doctor
```
Monatlich:
```bash
./scripts/alanctl backup
docker image prune -f
```
Vor jedem Hermes-Update:
```bash
./scripts/alanctl backup
./scripts/alanctl update-hermes
```
## Repo-Pflege
- `.editorconfig`, `CODEOWNERS` und `docs/maintainers.md` synchron halten
- `CHANGELOG.md` aktuell halten
- veraltete Screenshots/Beispiele entfernen
- `.env.example` synchron zur Doku halten
- neue Ports, Netze und Volumes zentral in README und Architektur dokumentieren
## Regelmaessige Sicherheitspruefung
- oeffentliche Bindings gegen `127.0.0.1` und `br0` gegenpruefen
- `SECURITY.md` bei geaendertem Meldeprozess aktualisieren
- nach Netz- oder Mount-Aenderungen gezielt `doctor` und einen Test-Upgrade-Lauf ausfuehren
+44
View File
@@ -0,0 +1,44 @@
# Parallelbetrieb mit OpenClaw
## Empfehlung
Lass OpenClaw in einem **separaten Compose-Projekt** laufen.
Das Repo hier geht davon aus, dass OpenClaw bereits existiert oder separat gepflegt wird. Dadurch bleiben Updates, Logik und Fehlersuche voneinander getrennt.
## Regeln gegen Konflikte
- andere Host-Ports als Hermes verwenden
- eigenes Volume oder eigener Basisordner fuer OpenClaw
- eigener Compose-Projektname
- wenn moeglich kein gemeinsamer Schreibzugriff auf denselben Host-Pfad
- Shared-Netz nur verwenden, wenn es einen echten Grund gibt
- fuer Internetzugang idealerweise dasselbe Muster verwenden: **eigenes externes Macvlan-Netz auf `br0` oder bewusst dasselbe externe `br0`-Netz mit eigener IP**
## Sichere Defaults
Hermes expose't nur lokal:
- `127.0.0.1:18642/tcp` fuer API/Health
- `127.0.0.1:19119/tcp` fuer Dashboard, falls aktiviert
OpenClaw sollte auf einem **anderen** Host-Port laufen, z. B. `127.0.0.1:18080` oder `127.0.0.1:18081`, je nach Image.
## `br0`-Hinweis
Wenn OpenClaw wie Hermes ueber `br0` ins Netz soll, hast du zwei saubere Optionen:
1. **gleiches externes Docker-Netz** verwenden, mit eigener statischer IP je Container
2. **separates zweites Macvlan-Netz** auf demselben Parent `br0` anlegen
Option 1 ist einfacher. Option 2 ist strenger getrennt.
## Wann das Shared-Netz sinnvoll ist
Nur wenn sich OpenClaw und Hermes wirklich gegenseitig erreichen muessen, z. B. fuer einen lokalen Upstream-Endpoint oder Kontrollpfad. Ansonsten reicht die komplette Trennung.
## Beispiel fuer OpenClaw
Siehe `compose/openclaw.override.example.yaml`.
Das ist bewusst nur eine Vorlage, weil der OpenClaw-Docker-Oekosystem-Stand momentan fragmentiert ist und verschiedene Images unterschiedliche Environment-Variablen erwarten.
+79
View File
@@ -0,0 +1,79 @@
# Release-Prozess
## Ziel
Dieses Repo soll sich wie ein kleines Infrastrukturprodukt pflegen lassen:
- Aenderungen gesammelt entwickeln
- lokal validieren
- in Gitea taggen
- Releases mit klaren Upgrade-Hinweisen veroeffentlichen
## Versionsschema
Empfohlen: semantische Versionierung fuer das Repo.
- `MAJOR`: inkompatible Struktur- oder Betriebswechsel
- `MINOR`: neue Features oder neue Betriebsfaelle ohne harte Brueche
- `PATCH`: Fixes, Doku, sichere Defaults, kleinere Skriptanpassungen
Beispiele:
- `v0.1.0` erste stabile Struktur
- `v0.2.0` neue optionalen Dienste oder neue Netzvarianten
- `v0.2.1` Bugfix im Update-/Backup-Verhalten
## Maintainer-Ablauf
1. Feature- oder Fix-Branch bauen
2. lokal testen
3. `CHANGELOG.md` aktualisieren
4. bei Upgrade-Auswirkungen `docs/upgrade-policy.md` pflegen
5. Merge nach `main`
6. Release-Tag setzen
7. Release Notes in Gitea anlegen
## Lokale Checks vor einem Release
```bash
./scripts/release-check
./scripts/alanctl validate
./scripts/alanctl doctor
```
Optional auf einem echten Host:
```bash
./scripts/alanctl backup
./scripts/alanctl up --dashboard
./scripts/alanctl update-hermes
```
## Tagging
```bash
git checkout main
git pull --ff-only
git tag -a v0.1.1 -m "alan-agents-workstation v0.1.1"
git push origin main --tags
```
## Inhalt guter Release Notes
- Was ist neu?
- Was ist gefixt?
- Muss `.env` angepasst werden?
- Gibt es ein neues Netzwerk-/Volume-/Port-Verhalten?
- Ist ein manuelles Backup vor Upgrade empfohlen?
- Gibt es ein Rollback-Risiko bei State-Migrationen?
## Release-Text vorbereiten
Nutze `docs/release-template.md` als portable Vorlage fuer die Release-Beschreibung in Gitea.
Empfohlener Ablauf:
1. `./scripts/new-release <version>` ausfuehren
2. `CHANGELOG.md` finalisieren
3. Release-Text aus `docs/release-template.md` kopieren und auf den konkreten Stand anpassen
4. Tag pushen und Release in Gitea anlegen
+52
View File
@@ -0,0 +1,52 @@
# Release Template
Nutze diese Vorlage fuer neue Gitea-Releases.
---
## Summary
Kurze Zusammenfassung des Releases in 2-4 Saetzen.
## Added
-
-
## Changed
-
-
## Fixed
-
-
## Operational impact
- Muss `.env` angepasst werden? Ja/Nein
- Neue Ports, Volumes oder Netzwerke? Ja/Nein
- Neue manuelle Schritte nach dem Update? Ja/Nein
## Upgrade notes
1. Optional: `./scripts/alanctl doctor`
2. Optional: `./scripts/alanctl backup`
3. Update ausfuehren: `./scripts/alanctl update-hermes`
4. Health pruefen: `./scripts/alanctl status`
## Rollback notes
- Image-Rollback: `./scripts/alanctl rollback-hermes`
- State-Restore nur dann, wenn eine Datenmigration oder Konfigurationsaenderung Probleme macht
## Maintainer checklist
- [ ] `CHANGELOG.md` aktualisiert
- [ ] Doku angepasst
- [ ] `./scripts/release-check` erfolgreich
- [ ] `./scripts/alanctl validate` erfolgreich
- [ ] Release-Tag gesetzt
---
+36
View File
@@ -0,0 +1,36 @@
# Update-Strategie
Dieses Repo trennt bewusst zwischen **Pinning**, **Upgrade**, **Health-Pruefung** und **Rollback**.
## Prinzip
- `.env` enthaelt den gewuenschten `HERMES_TAG`
- `pin-hermes <tag>` aendert nur den Ziel-Tag
- `update-hermes [tag]` sichert zuerst den State und fuehrt dann das Upgrade aus
- `rollback-hermes` rollt auf den zuletzt als gesund gespeicherten Stand zurueck
## Empfohlener Ablauf
Vor einem groesseren Update:
```bash
./scripts/alanctl doctor
./scripts/alanctl backup
./scripts/alanctl update-hermes v0.9.3
```
Wenn der neue Stand nicht sauber hochkommt:
```bash
./scripts/alanctl rollback-hermes
```
Wenn das Problem in einer State-Migration liegt:
```bash
./scripts/alanctl restore ./backups/hermes/hermes-YYYYMMDD-HHMMSS.tar.gz
```
## Warum nicht dauerhaft latest
`latest` ist bequem, aber fuer einen reproduzierbaren Desktop- oder Homeserver-Betrieb unnoetig riskant. Ein fixer Tag macht Updates nachvollziehbar und Rollbacks deutlich einfacher.
+93
View File
@@ -0,0 +1,93 @@
# Upgrade-Policy
## Grundsatz
Upgrades sollen reversibel, dokumentiert und fuer eine einzelne Workstation realistisch bedienbar sein.
## Klassen von Aenderungen
### 1. Sicheres Patch-Upgrade
Beispiele:
- Doku-Updates
- Healthcheck-Feinschliff
- nicht-invasive Skriptkorrekturen
Erwartung:
- kein manueller Eingriff noetig
- normales `update-hermes` reicht
### 2. Operatives Minor-Upgrade
Beispiele:
- neue optionale Dienste
- neue `.env`-Variablen mit Default
- neue Docs/Tools
Erwartung:
- Release Notes lesen
- `git diff .env.example` gegen lokale `.env` pruefen
- ggf. neuen Key ergaenzen
### 3. Breakendes Upgrade
Beispiele:
- Netzwerknamen aendern
- Compose-Service-Namen aendern
- persistente Pfade aendern
- State-/Datenmigrationen mit Rueckwirkungsrisiko
Erwartung:
- Backup Pflicht
- Upgrade-Fenster bewusst planen
- Rollback testen
- Release Notes muessen eine explizite Migrationsanleitung enthalten
## Pflicht vor jedem geplanten Upgrade
```bash
./scripts/alanctl doctor
./scripts/alanctl backup
```
## Pflicht bei Repo-Upgrade
Wenn du das Repo selbst aktualisierst:
```bash
git fetch --tags
git checkout <ziel-tag-oder-branch>
./scripts/release-check
```
Danach erst:
```bash
./scripts/alanctl validate
./scripts/alanctl up
```
## Umgang mit `.env`
Die lokale `.env` ist betriebsrelevant und bleibt lokal.
Vor jedem Repo-Upgrade:
```bash
git diff HEAD~1 .env.example
```
oder allgemeiner:
```bash
git diff <alter-tag>..<neuer-tag> -- .env.example
```
Wenn neue Variablen eingefuehrt wurden, muessen diese in die lokale `.env` uebernommen werden.
## Rollback-Regel
Image-Rollback und State-Rollback sind getrennt zu betrachten.
- `rollback-hermes` setzt das letzte bekannte gute Image wieder ein
- bei problematischer Datenmigration muss zusaetzlich ein Backup-Archiv via `restore` zurueckgespielt werden
+455
View File
@@ -0,0 +1,455 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ROOT_DIR}/.env"
ENV_EXAMPLE="${ROOT_DIR}/.env.example"
COMPOSE_FILE="${ROOT_DIR}/compose.yaml"
VERSIONS_FILE="${ROOT_DIR}/state/hermes/.last-known-good.env"
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
PROJECT_NAME="${COMPOSE_PROJECT_NAME:-alan}"
HERMES_DATA_DIR="${HERMES_DATA_DIR:-./state/hermes}"
HERMES_BACKUP_DIR="${HERMES_BACKUP_DIR:-./backups/hermes}"
HERMES_API_PORT="${HERMES_API_PORT:-18642}"
HERMES_DASHBOARD_PORT="${HERMES_DASHBOARD_PORT:-19119}"
AGENTS_SHARED_NETWORK="${AGENTS_SHARED_NETWORK:-alan_agents_shared}"
BR0_DOCKER_NETWORK="${BR0_DOCKER_NETWORK:-br0_lan}"
BR0_PARENT="${BR0_PARENT:-br0}"
BR0_SUBNET="${BR0_SUBNET:-}"
BR0_GATEWAY="${BR0_GATEWAY:-}"
BR0_IP_RANGE="${BR0_IP_RANGE:-}"
BR0_HOST_SHIM_CIDR="${BR0_HOST_SHIM_CIDR:-}"
BR0_HOST_SHIM_IFACE="${BR0_HOST_SHIM_IFACE:-${BR0_DOCKER_NETWORK}-shim}"
HERMES_IMAGE="${HERMES_IMAGE:-nousresearch/hermes-agent}"
HERMES_TAG="${HERMES_TAG:-latest}"
HEALTHCHECK_URL="http://127.0.0.1:${HERMES_API_PORT}/health"
compose() {
docker compose --project-name "$PROJECT_NAME" --env-file "$ENV_FILE" -f "$COMPOSE_FILE" "$@"
}
require_file() {
local path="$1"
[[ -f "$path" ]] || { echo "missing file: $path" >&2; exit 1; }
}
ensure_env() {
if [[ ! -f "$ENV_FILE" ]]; then
cp "$ENV_EXAMPLE" "$ENV_FILE"
echo "[OK] .env created from .env.example"
fi
}
ensure_dirs() {
mkdir -p "$ROOT_DIR/state/hermes" "$ROOT_DIR/backups/hermes"
}
ensure_networks() {
if ! docker network inspect "$AGENTS_SHARED_NETWORK" >/dev/null 2>&1; then
docker network create "$AGENTS_SHARED_NETWORK" >/dev/null
echo "[OK] created docker network ${AGENTS_SHARED_NETWORK}"
fi
if ! docker network inspect "$BR0_DOCKER_NETWORK" >/dev/null 2>&1; then
echo "[WARN] missing external br0 network ${BR0_DOCKER_NETWORK}"
echo "[HINT] run: ./scripts/alanctl net-create-br0"
fi
}
require_docker() {
command -v docker >/dev/null 2>&1 || { echo "docker not found" >&2; exit 1; }
docker info >/dev/null 2>&1 || { echo "docker daemon not reachable" >&2; exit 1; }
docker compose version >/dev/null 2>&1 || { echo "docker compose plugin not available" >&2; exit 1; }
}
port_in_use() {
local port="$1"
ss -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${port}$"
}
check_ports() {
local fail=0
for p in "$HERMES_API_PORT" "$HERMES_DASHBOARD_PORT"; do
if port_in_use "$p"; then
echo "[WARN] host port ${p} is already in use"
fail=1
fi
done
return "$fail"
}
show_openclaw_hint() {
if docker ps --format '{{.Names}}' | grep -Eqi 'openclaw'; then
echo "[INFO] OpenClaw container(s) detected. Shared network stays isolated by default; only explicit interconnects should use ${AGENTS_SHARED_NETWORK}."
fi
}
show_br0_hint() {
if ! ip link show "$BR0_PARENT" >/dev/null 2>&1; then
echo "[WARN] host bridge ${BR0_PARENT} not found"
fi
if docker network inspect "$BR0_DOCKER_NETWORK" >/dev/null 2>&1; then
echo "[OK] external br0 Docker network available: ${BR0_DOCKER_NETWORK}"
else
echo "[WARN] external br0 Docker network missing: ${BR0_DOCKER_NETWORK}"
fi
}
write_last_known_good() {
mkdir -p "$(dirname "$VERSIONS_FILE")"
cat > "$VERSIONS_FILE" <<EOV
HERMES_IMAGE=${HERMES_IMAGE}
HERMES_TAG=${HERMES_TAG}
EOV
echo "[OK] recorded last known good image ${HERMES_IMAGE}:${HERMES_TAG}"
}
read_last_known_good() {
require_file "$VERSIONS_FILE"
# shellcheck disable=SC1090
source "$VERSIONS_FILE"
echo "${HERMES_IMAGE}:${HERMES_TAG}"
}
health_wait() {
local timeout="${1:-120}"
local started
started="$(date +%s)"
while true; do
if curl -fsS "$HEALTHCHECK_URL" >/dev/null 2>&1; then
echo "[OK] Hermes healthy on ${HEALTHCHECK_URL}"
return 0
fi
if (( $(date +%s) - started >= timeout )); then
echo "[ERROR] Hermes health check timed out after ${timeout}s" >&2
return 1
fi
sleep 3
done
}
set_env_value() {
local key="$1"
local value="$2"
ensure_env
if grep -qE "^${key}=" "$ENV_FILE"; then
sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE"
else
printf '%s=%s\n' "$key" "$value" >> "$ENV_FILE"
fi
}
current_image_ref() {
echo "${HERMES_IMAGE}:${HERMES_TAG}"
}
cmd_bootstrap() {
require_docker
ensure_env
ensure_dirs
ensure_networks
check_ports || true
show_openclaw_hint
show_br0_hint
echo "[OK] bootstrap complete"
}
cmd_init_hermes() {
require_docker
ensure_env
ensure_dirs
echo "[INFO] starting Hermes setup wizard"
docker run -it --rm \
--network "$BR0_DOCKER_NETWORK" \
-v "${ROOT_DIR}/state/hermes:/opt/data" \
"${HERMES_IMAGE}:${HERMES_TAG}" setup
}
cmd_validate() {
require_docker
ensure_env
ensure_dirs
ensure_networks
echo "[INFO] validating compose"
compose config >/dev/null
check_ports || true
show_br0_hint
echo "[OK] compose config valid"
}
cmd_up() {
require_docker
ensure_env
ensure_dirs
ensure_networks
if ! docker network inspect "$BR0_DOCKER_NETWORK" >/dev/null 2>&1; then
echo "missing external br0 Docker network: ${BR0_DOCKER_NETWORK}" >&2
echo "create it with: ./scripts/alanctl net-create-br0" >&2
exit 1
fi
check_ports || true
local args=(up -d hermes)
if [[ "${1:-}" == "--dashboard" ]]; then
args=(--profile dashboard up -d hermes dashboard)
fi
compose "${args[@]}"
health_wait 120
write_last_known_good
}
cmd_down() {
require_docker
ensure_env
compose down
}
cmd_restart() {
require_docker
ensure_env
compose restart hermes
health_wait 120
}
cmd_status() {
require_docker
ensure_env
compose ps
echo
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | awk 'NR==1 || /alan|openclaw/'
if [[ -f "$VERSIONS_FILE" ]]; then
echo
echo "[INFO] last known good: $(read_last_known_good)"
fi
}
cmd_logs() {
require_docker
ensure_env
compose logs -f --tail=200 "${1:-hermes}"
}
cmd_shell() {
require_docker
ensure_env
docker exec -it "${PROJECT_NAME}-hermes" bash
}
cmd_doctor() {
require_docker
ensure_env
echo "[INFO] docker"
docker version --format '{{.Server.Version}}'
echo "[INFO] compose"
docker compose version
echo "[INFO] current image"
echo "$(current_image_ref)"
echo "[INFO] config check"
compose config >/dev/null && echo "[OK] compose config"
echo "[INFO] network"
docker network inspect "$AGENTS_SHARED_NETWORK" --format '{{.Name}}' 2>/dev/null || echo "missing: ${AGENTS_SHARED_NETWORK}"
docker network inspect "$BR0_DOCKER_NETWORK" --format '{{.Name}}' 2>/dev/null || echo "missing: ${BR0_DOCKER_NETWORK}"
echo "[INFO] ports"
check_ports || true
echo "[INFO] host bridge"
ip -brief link show "$BR0_PARENT" 2>/dev/null || echo "missing: ${BR0_PARENT}"
echo "[INFO] health"
if curl -fsS "$HEALTHCHECK_URL" >/dev/null 2>&1; then
echo "[OK] Hermes API health endpoint reachable on ${HERMES_API_PORT}"
else
echo "[WARN] Hermes API health endpoint not reachable on ${HERMES_API_PORT}"
fi
}
cmd_backup() {
ensure_env
ensure_dirs
local src="${ROOT_DIR}/state/hermes"
local dst_dir="${ROOT_DIR}/backups/hermes"
local ts
ts="$(date +%Y%m%d-%H%M%S)"
local archive="${dst_dir}/hermes-${ts}.tar.gz"
mkdir -p "$dst_dir"
tar -C "$ROOT_DIR/state" -czf "$archive" hermes
echo "$archive"
}
cmd_restore() {
ensure_env
ensure_dirs
local archive="${1:-}"
if [[ -z "$archive" || ! -f "$archive" ]]; then
echo "usage: $0 restore /path/to/hermes-backup.tar.gz" >&2
exit 1
fi
local restore_root="${ROOT_DIR}/state"
local current_backup
current_backup="$(cmd_backup)"
echo "[INFO] current state backed up to ${current_backup}"
rm -rf "${restore_root}/hermes"
tar -C "$restore_root" -xzf "$archive"
echo "[OK] restored ${archive}"
}
cmd_update_hermes() {
require_docker
ensure_env
ensure_dirs
local target_tag="${1:-$HERMES_TAG}"
local previous_tag="$HERMES_TAG"
local backup_file
backup_file="$(cmd_backup)"
echo "[INFO] backup created: ${backup_file}"
echo "[INFO] updating Hermes from ${HERMES_IMAGE}:${previous_tag} to ${HERMES_IMAGE}:${target_tag}"
set_env_value HERMES_TAG "$target_tag"
# reload updated .env
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
docker pull "${HERMES_IMAGE}:${target_tag}"
compose up -d --force-recreate hermes
if health_wait 180; then
write_last_known_good
echo "[OK] update successful"
return 0
fi
echo "[WARN] update failed health check, rolling back to ${previous_tag}"
set_env_value HERMES_TAG "$previous_tag"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
docker pull "${HERMES_IMAGE}:${previous_tag}" || true
compose up -d --force-recreate hermes
health_wait 180 || true
echo "[WARN] restored previous tag. If state migration caused issues, restore backup: ${backup_file}"
return 1
}
cmd_rollback_hermes() {
require_docker
ensure_env
local target_ref
target_ref="$(read_last_known_good)"
local target_tag="${target_ref##*:}"
echo "[INFO] rolling back to ${target_ref}"
set_env_value HERMES_TAG "$target_tag"
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
docker pull "$target_ref"
compose up -d --force-recreate hermes
health_wait 180
}
cmd_pin_hermes() {
ensure_env
local tag="${1:-}"
if [[ -z "$tag" ]]; then
echo "usage: $0 pin-hermes <tag>" >&2
exit 1
fi
set_env_value HERMES_TAG "$tag"
echo "[OK] pinned HERMES_TAG=${tag} in .env"
}
cmd_net_create_br0() {
require_docker
ensure_env
if [[ -z "$BR0_SUBNET" || -z "$BR0_GATEWAY" || -z "$BR0_IP_RANGE" ]]; then
echo "BR0_SUBNET, BR0_GATEWAY and BR0_IP_RANGE must be set in .env" >&2
exit 1
fi
if ! ip link show "$BR0_PARENT" >/dev/null 2>&1; then
echo "host bridge not found: ${BR0_PARENT}" >&2
exit 1
fi
if docker network inspect "$BR0_DOCKER_NETWORK" >/dev/null 2>&1; then
echo "[OK] ${BR0_DOCKER_NETWORK} already exists"
return 0
fi
docker network create -d macvlan \
--subnet "$BR0_SUBNET" \
--gateway "$BR0_GATEWAY" \
--ip-range "$BR0_IP_RANGE" \
-o parent="$BR0_PARENT" \
"$BR0_DOCKER_NETWORK" >/dev/null
echo "[OK] created macvlan network ${BR0_DOCKER_NETWORK} on parent ${BR0_PARENT}"
}
cmd_net_create_shim() {
if [[ -z "$BR0_HOST_SHIM_CIDR" ]]; then
echo "BR0_HOST_SHIM_CIDR must be set in .env" >&2
exit 1
fi
if ! docker network inspect "$BR0_DOCKER_NETWORK" >/dev/null 2>&1; then
echo "missing Docker network: ${BR0_DOCKER_NETWORK}" >&2
exit 1
fi
sudo ip link add "$BR0_HOST_SHIM_IFACE" link "$BR0_PARENT" type macvlan mode bridge 2>/dev/null || true
sudo ip addr replace "$BR0_HOST_SHIM_CIDR" dev "$BR0_HOST_SHIM_IFACE"
sudo ip link set "$BR0_HOST_SHIM_IFACE" up
if [[ -n "$BR0_IP_RANGE" ]]; then
sudo ip route replace "$BR0_IP_RANGE" dev "$BR0_HOST_SHIM_IFACE"
fi
echo "[OK] host shim ready on ${BR0_HOST_SHIM_IFACE} (${BR0_HOST_SHIM_CIDR})"
}
usage() {
cat <<USAGE
Usage: $(basename "$0") <command>
Commands:
bootstrap Prepare directories, .env and Docker networks
init-hermes Run Hermes setup wizard interactively
validate Validate compose config and port availability
up [--dashboard] Start Hermes, optionally with dashboard
down Stop the stack
restart Restart Hermes
status Show compose and related container status
logs [service] Tail logs (default: hermes)
shell Open a shell inside the Hermes container
doctor Health and environment checks
backup Create a timestamped backup archive
restore <file> Restore Hermes state from backup archive
update-hermes [tag] Backup, pull target tag, recreate service, verify health, auto-rollback on failure
rollback-hermes Reapply the last known good image tag
pin-hermes <tag> Persist a specific Hermes image tag in .env
net-create-br0 Create the external macvlan Docker network on top of br0
net-create-shim Create optional host-side macvlan shim for host<->container access
USAGE
}
main() {
local cmd="${1:-}"
shift || true
case "$cmd" in
bootstrap) cmd_bootstrap "$@" ;;
init-hermes) cmd_init_hermes "$@" ;;
validate) cmd_validate "$@" ;;
up) cmd_up "$@" ;;
down) cmd_down "$@" ;;
restart) cmd_restart "$@" ;;
status) cmd_status "$@" ;;
logs) cmd_logs "$@" ;;
shell) cmd_shell "$@" ;;
doctor) cmd_doctor "$@" ;;
backup) cmd_backup "$@" ;;
restore) cmd_restore "$@" ;;
update-hermes) cmd_update_hermes "$@" ;;
rollback-hermes) cmd_rollback_hermes "$@" ;;
pin-hermes) cmd_pin_hermes "$@" ;;
net-create-br0) cmd_net_create_br0 "$@" ;;
net-create-shim) cmd_net_create_shim "$@" ;;
*) usage; [[ -n "$cmd" ]] && exit 1 ;;
esac
}
main "$@"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
version="${1:-}"
release_date="${2:-$(date +%F)}"
if [[ -z "$version" ]]; then
echo "usage: $0 <version-without-v> [YYYY-MM-DD]" >&2
exit 1
fi
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "version must look like 0.2.1" >&2
exit 1
fi
if grep -q "## \[$version\]" CHANGELOG.md; then
echo "version already exists in CHANGELOG.md" >&2
exit 1
fi
python3 - <<PY
from pathlib import Path
p = Path("CHANGELOG.md")
text = p.read_text()
needle = "## [Unreleased]\n\n### Added\n- Platzhalter fuer neue Features\n\n### Changed\n- Platzhalter fuer geaenderte Betriebslogik\n\n### Fixed\n- Platzhalter fuer Bugfixes\n\n### Security\n- Platzhalter fuer sicherheitsrelevante Anpassungen\n"
if needle not in text:
raise SystemExit("Unreleased template not found exactly; update CHANGELOG manually")
replacement = needle + f"\n## [{version}] - {release_date}\n\n### Added\n-\n\n### Changed\n-\n\n### Fixed\n-\n\n### Security\n-\n"
p.write_text(text.replace(needle, replacement, 1))
PY
echo "[OK] prepared CHANGELOG entry for $version on $release_date"
echo "[NEXT] fill the new release section, commit, then tag with: git tag -a v${version} -m 'v${version}'"
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
fail() {
echo "[ERROR] $*" >&2
exit 1
}
[[ -f README.md ]] || fail "README.md missing"
[[ -f CHANGELOG.md ]] || fail "CHANGELOG.md missing"
[[ -f compose.yaml ]] || fail "compose.yaml missing"
[[ -f .env.example ]] || fail ".env.example missing"
[[ -x scripts/alanctl ]] || fail "scripts/alanctl missing or not executable"
bash -n scripts/alanctl
if [[ -f scripts/new-release ]]; then
bash -n scripts/new-release
fi
grep -q '^## \[Unreleased\]' CHANGELOG.md || fail "CHANGELOG.md needs an [Unreleased] section"
grep -q 'HERMES_TAG=' .env.example || fail ".env.example should document HERMES_TAG"
grep -q 'BR0_DOCKER_NETWORK=' .env.example || fail ".env.example should document BR0_DOCKER_NETWORK"
if command -v docker >/dev/null 2>&1; then
docker compose -f compose.yaml config >/dev/null
else
echo "[WARN] docker not found; skipped compose validation"
fi
echo "[OK] release-check passed"
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Alan Hermes Docker Stack
After=default.target docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=%h/src/alan
ExecStart=%h/src/alan/scripts/alanctl up
ExecStop=%h/src/alan/scripts/alanctl down
TimeoutStartSec=180
TimeoutStopSec=60
[Install]
WantedBy=default.target