From 7f9b0e6c5f4f25334f0142728c378bf556096756 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Tue, 21 Apr 2026 11:53:29 +0200 Subject: [PATCH] Initial version of the Hermes self learning agent setup of JR IT Services. --- .editorconfig | 30 ++ .env.example | 56 +++ .gitea/ISSUE_TEMPLATE/bug_report.yaml | 35 ++ .gitea/ISSUE_TEMPLATE/feature_request.yaml | 24 ++ .gitea/pull_request_template.md | 18 + .gitea/workflows/repo-check.yml | 36 ++ .gitignore | 8 + CHANGELOG.md | 27 ++ CODEOWNERS | 20 + CONTRIBUTING.md | 50 +++ LICENSE | 21 + Makefile | 39 ++ README.md | 228 +++++++++++ SECURITY.md | 65 +++ compose.yaml | 70 ++++ compose/openclaw.override.example.yaml | 38 ++ docs/architecture.md | 63 +++ docs/bridge-br0.md | 66 +++ docs/gitea-import.md | 84 ++++ docs/hermes.md | 57 +++ docs/maintainers.md | 47 +++ docs/maintenance.md | 38 ++ docs/openclaw-coexistence.md | 44 ++ docs/release-process.md | 79 ++++ docs/release-template.md | 52 +++ docs/update-strategy.md | 36 ++ docs/upgrade-policy.md | 93 +++++ scripts/alanctl | 455 +++++++++++++++++++++ scripts/new-release | 37 ++ scripts/release-check | 34 ++ systemd/user/alan-hermes.service | 16 + 31 files changed, 1966 insertions(+) create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 .gitea/pull_request_template.md create mode 100644 .gitea/workflows/repo-check.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 compose.yaml create mode 100644 compose/openclaw.override.example.yaml create mode 100644 docs/architecture.md create mode 100644 docs/bridge-br0.md create mode 100644 docs/gitea-import.md create mode 100644 docs/hermes.md create mode 100644 docs/maintainers.md create mode 100644 docs/maintenance.md create mode 100644 docs/openclaw-coexistence.md create mode 100644 docs/release-process.md create mode 100644 docs/release-template.md create mode 100644 docs/update-strategy.md create mode 100644 docs/upgrade-policy.md create mode 100755 scripts/alanctl create mode 100755 scripts/new-release create mode 100755 scripts/release-check create mode 100644 systemd/user/alan-hermes.service diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8c2ccc5 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..87311eb --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.yaml b/.gitea/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..6cf04bd --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.yaml @@ -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 diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.yaml b/.gitea/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..e14b600 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.yaml @@ -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`? diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..1857b68 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -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 diff --git a/.gitea/workflows/repo-check.yml b/.gitea/workflows/repo-check.yml new file mode 100644 index 0000000..b20cddf --- /dev/null +++ b/.gitea/workflows/repo-check.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a41c8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.env +.env.local +backups/ +state/ +third_party/ +*.log +*.tar.gz +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..edef51b --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..2a13e11 --- /dev/null +++ b/CODEOWNERS @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d8fffc1 --- /dev/null +++ b/CONTRIBUTING.md @@ -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`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75a091c --- /dev/null +++ b/LICENSE @@ -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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..74fe6d0 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3f6303 --- /dev/null +++ b/README.md @@ -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`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d76d123 --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1c7a959 --- /dev/null +++ b/compose.yaml @@ -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 diff --git a/compose/openclaw.override.example.yaml b/compose/openclaw.override.example.yaml new file mode 100644 index 0000000..ae98fdf --- /dev/null +++ b/compose/openclaw.override.example.yaml @@ -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 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..32cab36 --- /dev/null +++ b/docs/architecture.md @@ -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. diff --git a/docs/bridge-br0.md b/docs/bridge-br0.md new file mode 100644 index 0000000..ebfcb76 --- /dev/null +++ b/docs/bridge-br0.md @@ -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 diff --git a/docs/gitea-import.md b/docs/gitea-import.md new file mode 100644 index 0000000..9e96356 --- /dev/null +++ b/docs/gitea-import.md @@ -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@//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 +``` diff --git a/docs/hermes.md b/docs/hermes.md new file mode 100644 index 0000000..b951b50 --- /dev/null +++ b/docs/hermes.md @@ -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. diff --git a/docs/maintainers.md b/docs/maintainers.md new file mode 100644 index 0000000..334f975 --- /dev/null +++ b/docs/maintainers.md @@ -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 diff --git a/docs/maintenance.md b/docs/maintenance.md new file mode 100644 index 0000000..ed74d88 --- /dev/null +++ b/docs/maintenance.md @@ -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 diff --git a/docs/openclaw-coexistence.md b/docs/openclaw-coexistence.md new file mode 100644 index 0000000..611c12a --- /dev/null +++ b/docs/openclaw-coexistence.md @@ -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. diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..8de45d5 --- /dev/null +++ b/docs/release-process.md @@ -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 ` 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 diff --git a/docs/release-template.md b/docs/release-template.md new file mode 100644 index 0000000..0e4d598 --- /dev/null +++ b/docs/release-template.md @@ -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 + +--- diff --git a/docs/update-strategy.md b/docs/update-strategy.md new file mode 100644 index 0000000..1fe932f --- /dev/null +++ b/docs/update-strategy.md @@ -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 ` 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. diff --git a/docs/upgrade-policy.md b/docs/upgrade-policy.md new file mode 100644 index 0000000..115a1b8 --- /dev/null +++ b/docs/upgrade-policy.md @@ -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 +./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 .. -- .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 diff --git a/scripts/alanctl b/scripts/alanctl new file mode 100755 index 0000000..46794c1 --- /dev/null +++ b/scripts/alanctl @@ -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" </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 " >&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 < + +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 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 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 "$@" diff --git a/scripts/new-release b/scripts/new-release new file mode 100755 index 0000000..3a98eb9 --- /dev/null +++ b/scripts/new-release @@ -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 [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 - <&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" diff --git a/systemd/user/alan-hermes.service b/systemd/user/alan-hermes.service new file mode 100644 index 0000000..5c7beed --- /dev/null +++ b/systemd/user/alan-hermes.service @@ -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