# Modul-17: Hermes-Agent (autonomer Research-/Analyse-Worker unter Paperclip) **Status: FREIGEGEBEN (21.08.2026)** · Container `Modul-17-Hermes-Agent` · Image `trading-modules-modul17-hermes-agent:latest` > ⚠️ **Keine Namenskollision** mit fremden Containern. Hermes läuft im `trading-modules`-Netz > als `Modul-17-Hermes-Agent`. Fremdsysteme (`paperclip-dn8l-paperclip-1` u.ä., Coolify) nie anfassen. ## Zweck & Rolle Hermes ist die **autonome Research-/Analyse-Ausführungsebene unter Paperclip (M16)**. Er nimmt von Paperclip delegierte Tasks (Research, längere Analysen, Strategie-Review, Analytics-Auswertung, Backtest-/Optimization-Auswertung, Systemdiagnose READ-ONLY) entgegen und führt sie **ausschließlich über die strikte Tool-Whitelist** aus. Das Ergebnis geht als `agent_run`/`result_refs` an Paperclip zurück. **Kritische Grenze — Hermes ist NICHT Teil des kritischen Trading-Pfads und darf NICHT:** - direkt auf Modul-09 Execution zugreifen, Orders erzeugen - Risk-/Portfolio-Regeln (M07/M08) überschreiben - M15-Control verändern (PAUSE/HALT/RESUME) - Strategien automatisch produktiv aktivieren - Optimierungsparameter automatisch deployen - Broker-Credentials erhalten **Paperclip bleibt Orchestrator.** Hermes arbeitet NUR über die erlaubten Tools/APIs. Unbekannte oder unerlaubte Tools → **REJECTED**. ## Architektur / Datenfluss ``` Paperclip (M16) ── Task ──▶ Modul-17-Hermes-Agent (Port 55017, NUR intern/expose) │ ├── app/core/tool_whitelist.py strikte Tool-Whitelist (Herzstück) ├── app/core/executor.py Hermes-Executor (Tool-Ausführung + Audit + Idempotenz) ├── app/storage/storage.py Persistenz hermes_task/run/tool_call/result ├── app/clients/clients.py Read-Clients + 2 begrenzte Write-Clients └── app/api/main.py REST-API (/health, /tasks, /runs, /tools) │ ├── READ (tolerant): Modul-03 Market Data, M04 Regime, M05 Strategy, │ M06 Ranking, M10 Journal, M11 Analytics, M15 Monitoring (GET) └── WRITE (begrenzt): Modul-12 POST /backtest, Modul-13 POST /optimization, Proposal/Research-Ergebnis speichern ``` **API (Docker-intern, Port 55017):** - `GET /health` — Liveness (`{"status":"ok","service":"hermes","version":"0.1.0"}`) - `GET /health/ready` — Readiness (DB-Schema vorhanden) → 200 `{"status":"ready"}` - `POST /tasks` — Task einreichen (`task_type` + `payload.tools[]`); Whitelist-Prüfung - `GET /tasks` / `GET /tasks/{id}` — Tasks + Status/result_refs - `GET /runs/{id}` — einzelner Run mit result_refs - `GET /tools` — Tool-Whitelist auflisten Kein RabbitMQ: synchroner request/response. Port 55017 **nur intern** (`expose:`), kein öffentliches Mapping. ## Tool-Whitelist (strikt, `app/core/tool_whitelist.py`) Jeder Tool-Call wird gegen die Whitelist geprüft. Unbekannte/unerlaubte Tools → REJECTED. | Tool (namespace.method) | Zielmodul | Richtung | Status | |-------------------------|-----------|----------|--------| | `market_data.prices` | Modul-03 | READ | erlaubt | | `regime.analyze` | Modul-04 | READ | erlaubt | | `strategy.list` | Modul-05 | READ | erlaubt | | `ranking.current` | Modul-06 | READ | erlaubt | | `journal.entries` | Modul-10 | READ | erlaubt | | `analytics.portfolio` | Modul-11 | READ | erlaubt | | `monitoring.status` | Modul-15 | READ | erlaubt (NUR GET) | | `backtest.start` | Modul-12 | WRITE | erlaubt (CONTROLLED) | | `optimization.start` | Modul-13 | WRITE | erlaubt (CONTROLLED) | | `execution.place_order` | Modul-09 | WRITE | **REJECTED** | | `risk.set_limits` | Modul-07 | WRITE | **REJECTED** | | `portfolio.rebalance` | Modul-08 | WRITE | **REJECTED** | | `monitoring.pause/halt/resume` | Modul-15 | Control | **REJECTED** | | alles andere | — | — | **REJECTED** | **M09/M07/M08 sind strukturell NICHT in den Clients implementiert** (`clients.py`) — ein Verstoß ist damit zur Laufzeit unmöglich, zusätzlich zur Whitelist-Barriere. M15-Control-Writes fehlen ebenfalls. ## Persistenz / Audit (Migration `migrations/001_hermes.sql`) Vier Tabellen in Modul-01-PostgreSQL (PostgreSQL-01), NICHT im kritischen Trading-Pfad: - `hermes_task` — Aufgaben (id, task_type, agent_role, status, payload, result_refs, error_code) - `hermes_run` — Ausführungen (run_id, task_id, status, result, result_refs, idempotent) - `hermes_tool_call` — **jeder Tool-Call nachvollziehbar**: task_id, tool, target_module, input_hash, status, timestamps, error_code - `hermes_result` — gespeicherte Ergebnisse/Reports Jeder Tool-Call wird in `hermes_tool_call` auditier. Keine Secrets/Prompts mit Credentials in Logs. ## Sicherheit - **Keine öffentlichen Ports**: 55017 nur `expose` (Docker-intern), kein Host-Mapping (verifiziert: `docker port` leer). - **Internes Docker-Netzwerk** `trading-modules`. - **Kein Docker-Socket, kein Root-/Shell-Zugriff auf andere Container.** - **Secrets nur ENV/Secret** (`PG_PASSWORD` aus Compose `environment`); keine Secrets in Logs/API/DB. - **Keine Broker-Credentials** (kein Client, kein ENV dafür). - **Hermes-Ausfall beeinflusst M03–16 NICHT** (eigene DB-Tabellen, keine Kopplung an Trading-Core). - **M15-Ausfall → Monitoring-/Systemdiagnose-Task FAILED** (FAIL-CLOSED, nicht schönreden). ## Read-Clients (tolerant) Einzelne Quellen-Fehler (404/5xx/Timeout) werden als Teil-Ergebnis `{"_error": code}` gekapselt, der Gesamt-Task bleibt SUCCEEDED. Komplett unreachable (`UNREACHABLE`/`TIMEOUT`) → `None`. **Ausnahme — Monitoring (FAIL-CLOSED):** Der `monitoring.status`-Call ruft M15 direkt auf. Ist M15 nicht erreichbar → Task **FAILED** (kein "ok" bei ausgefallenem M15). Gleiches gilt für M12/M13: Ausfall → zugehöriger Backtest-/Optimization-Task **FAILED** (`UNREACHABLE`), Hermes bleibt healthy, Trading-Core unbeeinflusst. ## Idempotenz Jeder `task_id` erzeugt genau **einen** `hermes_run` (SUCCEEDED). Wiederholte Einreichung derselben Task-Payload → je Task genau 1 Run, kein Doppel-Run. `input_hash` in `hermes_tool_call` identifiziert identische Aufrufe. ## Tests & E2E-Verifikation (VPS, 21.08.2026) - **Unit-Tests** `tests/test_hermes.py`: 13/13 grün (Whitelist-Gates, Executor, Idempotenz, REJECTED). - **T1 Research-Task** → M03 → **SUCCEEDED**, result_refs mit `tool_output` + `report`. - **T2 Analytics-Task** → M11 → **SUCCEEDED**. - **T3 Backtest-Task** → M12 `POST /backtest` (BT_PULL, stock, 450 Candles) → **SUCCEEDED**. - **T4 Optimization-Task** → M13 `POST /optimization` (BT_PULL) → **SUCCEEDED**. - **T5 Monitoring READ-ONLY** → M15 `GET /status` → **SUCCEEDED**. - **T6 Execution-Tool** (`execution.place_order`) → **REJECTED** (`TOOL_NOT_ALLOWED`). - **T7 M15-Control-Tool** (`monitoring.pause`) → **REJECTED**. - **T8 Risk/Portfolio-Write** (`risk.set_limits`) → **REJECTED**. - **T9 ungültiges Tool** (`bogus.tool`) → **REJECTED**. - **T10 Ausfall**: M15 down → Monitoring FAILED; M12 down → Backtest FAILED; M13 down → Optimization FAILED (jeweils `UNREACHABLE`). Erholung → SUCCEEDED. Hermes bleibt healthy. - **T11 Idempotenz**: je Task genau 1 Run. - **T12 Restart/Reconnect**: nach `docker restart` ready, Daten persistent. - **T13 keine Secrets**: `docker logs` frei von password/secret/token/api_key. - **T14 Port nur intern**: `ss -ltn` kein Host-Listening auf 55017. - **T15 Hermes-Ausfall**: bei gestopptem Hermes sind M03/04/05/11/12/13/15/16 alle healthy (`/health` ok) → Trading-Core unbeeinflusst. **E2E-Gesamt: 15/15 grün.** ## Bugs / Fixes (modul-17, dokumentiert) 1. **Methodenname `_last_succeeded_run` vs `_run_last_succeeded_run`**: Executor referenzierte `_last_succeeded_run`, Methode hieß anders → benannt in `_last_succeeded_run` (Idempotenz-Check). 2. **Test-Pfad**: `sys.path.insert` musste auf `../app` zeigen (nicht Modul-Root) für `app.*`-Importe. 3. **M12 422 "Unzureichende Daten"**: EURUSD/demo lieferte nur 1 Candlestick (< 220 nötig). Fix im E2E: Symbol **`BT_PULL`** (asset_class `stock`, 450 Candles) für Backtest/Optimization. 4. **M13 erfordert Pflichtfelder `start_date`+`end_date`** (anders als M12): Optimization-Payload musste beide Datumsfelder enthalten, sonst HTTP 422. 5. **E2E-Skript**: BusyBox wget im Alpine-Worker kennt kein `--post-file` → JSON per `docker cp` + `--post-data="$(cat …)"`. Funktionsname `mkpayload`→`mk_put` (aufrufender Code nutzte `mk_put`). 6. **SSH-Quoting**: verschachtelte `$(…)` mit doppelten Anführungszeichen im Inline-Python kappen das Kommando → Inline-Echo entfernt, reiner grep-Check. ## Compose / Betrieb - Netz `trading-modules`, DB-Host `Modul-01-PostgreSQL` (Modul-01), Port 55017 nur intern (`expose`), `restart: unless-stopped`. Kein öffentliches Port-Mapping. - **Kein Teil des kritischen Trading-Pfads:** kein M09-, kein M07/M08-, kein M15-Control-Zugriff. - Paperclip (M16) bleibt Orchestrator; Hermes ist seine Worker-Ausführungsebene. --- ## LLM-Schicht (KI-/Analyse-Erweiterung, M16 additiv `llm=true`) Hermes kann Tasks zusätzlich über eine **LLM-Schicht** (Tool-Calling-Loop) ausführen. Die LLM-Schicht ist **additiv** (M16 setzt `llm=true` im Task-Payload); ohne Flag bleibt das deterministische M17-Verhalten unverändert. Die Tool-Whitelist und Task-Gates bleiben **deterministisch außerhalb des LLM** — das LLM führt nie selbst Tools aus, sondern liefert `requested_tools[]`, die durch die bestehende Whitelist geprüft werden. ### 2-Stufen-Modellkonzept (final, 21.08.2026) | Stufe | Modell | Provider | Verwendung | |-------|--------|----------|------------| | **STANDARD** | `deepseek-v4-flash:cloud` | `ollama_cloud` | Normale Research-/Analyse-Tasks, voller Tool-Calling-Loop, Structured JSON, vollständiger Audit | | **LOKAL** | `llama3.1:8b` | `ollama_local` | NUR expliziter Privacy-/Offline-Modus (`llm_mode=local`), **Analyse-only** (kein Tool-Loop), CPU-/Timeout-Schutz | **Regeln (hart):** - **KEIN automatischer Cloud→Local-Fallback.** Cloud-Ausfall → Task **FAILED** (`LLM_UNAVAILABLE`), kein minutenlanger Wechsel auf das lokale Modell. - **LOKAL nur explizit** über `llm_mode=local` im Task-Payload (`extra.llm_mode`). Nicht als Standard verwenden. - **LOKAL = Analyse-only**: genau EIN Analyse-Call, keine Tool-Anfragen. `llama3.1:8b` auf CPU versteht den Tool-Calling-Loop nicht zuverlässig (LLM_MAX_ROUNDS) und ist langsam (~60-90s/Call). - **FAIL-CLOSED**: LLM down/Timeout/invalid JSON/Schema/Max-Runden → Task FAILED, kein Trading-Core-Effekt. - **Safety unverändert**: kein `execution.place_order`, kein `risk.set_limits`, kein `portfolio.rebalance`, kein M15 pause/halt/resume, Proposal maximal DRAFT/PROPOSED. ### LLM-Audit (`hermes_llm_run`, Migration `002_hermes_llm.sql`) Fünfte Audit-Tabelle. Je LLM-Run: provider, model, config_version, input_context_hash, requested_tools, executed_tools, rejected_tools, tokens_in/out, latency_ms, structured_result, status, error_code. **Vollständig AUCH im FAILED-Pfad** (Timeout/Invalid-JSON/Max-Runden persistieren Metriken). Keine vollständigen sensiblen Prompts, keine Secrets. ### LLM-ENV (Compose M17-Block) - `LLM_PROVIDER=ollama_cloud` (Standard = Cloud) - `LLM_MODEL_CLOUD=deepseek-v4-flash:cloud` - `LLM_MODEL_LOCAL=llama3.1:8b` - `LLM_BASE_URL=http://10.0.4.2:11434` (Ollama-Netz) - `LLM_TIMEOUT_SECONDS=300` ### LLM-E2E-Verifikation (final, 21.08.2026) - **Cloud-Standard** (`llm=true`, standard): **SUCCEEDED** (deepseek-v4-flash:cloud, 4 requested/2 executed, Audit vollständig). - **Local explizit** (`llm=true`, `llm_mode=local`): **SUCCEEDED** (llama3.1:8b, Analyse-only, Audit vollständig). - **Cloud-down** (isoliert, ungültige Base-URL): **FAILED** (`LLM_UNAVAILABLE`), **kein Auto-Fallback** (Provider bleibt Cloud). - **llm=false**: **SUCCEEDED** (deterministisches M17, kein LLM-Run). - **Audit vollständig**: requested/executed/rejected/tokens/latency/context_hash persistiert, auch im FAILED-Pfad. - **M03–M15 unbeeinflusst**: alle Module healthy während der LLM-Tests. - **Unit-Tests** `tests/test_llm.py`: **20/20 grün** (inkl. 4 REJECTED-Gate-Tests: place_order, monitoring_pause, risk_set_limits, unknown_tool). ### LLM-Bugs / Fixes 1. **Audit-Metriken leer (Cloud-E2E)**: `finish_llm_run` schrieb nur status/result/error → um requested/executed/rejected_tools, tokens_in/out, latency_ms, structured_result, provider/model/config_version erweitert. 2. **FAILED-Pfad verlor Audit-Metriken**: Max-Runden-Exception trug keine Metriken → Exception trägt jetzt Audit-Dict, `execute()` persistiert sie auch bei LLM_MAX_ROUNDS/Timeout/Invalid-JSON. 3. **NOT NULL-Constraint**: `requested_tools` etc. `NOT NULL DEFAULT '[]'` → `finish_llm_run` koerziert `None`→`[]`. 4. **Structured Output**: Cloud-Modell ignoriert Schema ohne `format`-Constraint → `format`-Constraint im Payload. 5. **ToolCall-Format**: ToolCall-Modell erwartet `name`/`args` (nicht `tool`/`params`) → Skript/Executor angepasst. 6. **Lokales Modell ungeeignet für Tool-Loop**: `llama3.1:8b` auf CPU → LLM_MAX_ROUNDS (fragt endlos Tools an). Fix: lokaler Modus = **Analyse-only** (kein Tool-Loop), wie vom User empfohlen. ## Geändert ``` Geändert von: Rain Ocampo Datum: 21.08.2026 Grund: LLM-Schicht (2-Stufen-Konzept) ergaenzt: STANDARD=deepseek-v4-flash:cloud (voller Tool-Loop), LOKAL=llama3.1:8b (nur explizit llm_mode=local, Analyse-only), KEIN Auto-Fallback, Cloud-down->FAILED (LLM_UNAVAILABLE), llm=false deterministisch, Audit hermes_llm_run vollstaendig (auch FAILED-Pfad), Unit 20/20, E2E 5/5 Szenarien gruen, M03-M15 unbeeinflusst. FREIGEGEBEN. Geändert von: Rain Ocampo Datum: 21.08.2026 Grund: Modul-17-Hermes-Agent implementiert + E2E verifiziert (Research/Analytics/Backtest/Optimization/Monitoring-Success, Safety REJECTED 6/7/8/9, M12/M13/M15-Ausfall -> FAILED (FAIL-CLOSED), Idempotenz, Restart, keine Secrets, Port intern, Hermes-Ausfall beeinflusst M03-16 nicht). Tool-Whitelist strikt, 4 Audit-Tabellen (hermes_task/run/tool_call/result), 6 API-Endpunkte. Unit 13/13, E2E 15/15 gruen. FREIGEGEBEN. Keine weiteren Module begonnen. ```