commit 7f9b0e6c5f4f25334f0142728c378bf556096756 Author: Johannes Rest Date: Tue Apr 21 11:53:29 2026 +0200 Initial version of the Hermes self learning agent setup of JR IT Services. 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