UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4

Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem
Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag.

Oberflaeche
- Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als
  mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml
  (eigene ControlThemes statt Fluent umzufaerben).
- Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer
  Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite
  statt eigenem Fenster, Texteditor-Fenster.
- ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter
  entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt.

Rocket.Chat
- Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der
  Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird
  nur, wenn wirklich etwas anliegt.
- send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum
  ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist.
- Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei
  Annahmen waren falsch und sind korrigiert.

Deploymentcenter
- DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json)
  erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke.
- AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst
  ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:16:05 +02:00
co-authored by Claude Opus 5
parent b5bf97ae74
commit 33d95a6c3f
88 changed files with 11665 additions and 1029 deletions
+219 -5
View File
@@ -62,6 +62,104 @@ Ebenso vorhanden und wiederverwendbar:
Geprüft gegen die REST- und Realtime-API von Rocket.Chat. Alles Folgende ist
Standardfunktion einer selbstgehosteten Instanz, kein Enterprise-Feature.
### 2.0 Prüfung gegen die echte Instanz (Rocket.Chat 8.7, August 2026)
Alles unten Stehende wurde gegen die Testinstanz gemessen, nicht aus der Dokumentation
übernommen. **Drei Annahmen waren falsch** — sie sind hier korrigiert.
**Bestätigt:**
| Prüfung | Ergebnis |
|---|---|
| `subscriptions.get` als Sammelabruf | liefert je Raum `rid`, `t`, `name`, `unread`, `userMentions`, `groupMentions`, `alert` — genau der Vorfilter, auf dem der Poll steht |
| `chat.postMessage`, auch mit `tmid` (Thread) | funktioniert |
| `channels.history` / `groups.history` / `im.history` mit `oldest` | funktioniert; Raumart bestimmt den Endpunkt |
| `subscriptions.read` | funktioniert; danach steht `ls` und `unread` fällt auf 0 |
| `im.create`, `groups.create`, Senden in privaten Gruppen | funktioniert |
| Erwähnungen | kommen als **`mentions[]`-Feld mit Benutzernamen** — der Erwähnungsfilter braucht kein Textparsen |
| Unbekannter Raum | `400 [invalid-channel]` |
| `users.create` mit Rolle `bot` | funktioniert (Admin) |
**Korrekturen:**
1. **Berechtigungsnamen.** Sie heißen `create-personal-access-tokens` (Rollen: `admin`,
`user`) und `user-generate-access-token` (Rolle: `admin`) — nicht wie zuvor notiert.
2. **Ein Admin kann *kein* Token für einen fremden Benutzer prägen.**
`users.generatePersonalAccessToken` lehnt `userId` ab („must NOT have additional
properties") — der Endpunkt gilt nur für den aufrufenden Benutzer.
`users.createToken` verlangt ein `secret`, für das es in dieser Instanz keine
Einstellung gibt. Beide Wege sind zu.
3. **Systemnachrichten.** Die Historie liefert auch Ereignisse wie „Benutzer beigetreten"
(Feld `t`, z. B. `uj`). Ohne Filter antwortet ein Agent auf einen Raumbeitritt. Im
Tool umgesetzt und geprüft.
**Offen geblieben:** Das Ratenlimit (`API_Enable_Rate_Limiter` = an, 10 Aufrufe/60 s)
griff bei 14 schnellen Aufrufen **nicht** — Administratoren umgehen es. Für einen
Benutzer mit reiner `bot`-Rolle ist es damit **nicht** gemessen. Der Entwurf bleibt mit
einem Sammelabruf je Takt weit darunter; nachzumessen, sobald ein Agenten-Benutzer
nutzbar ist.
### 2.0.1 Der Stolperstein: 2FA verhindert die automatische Bereitstellung
Ein frisch per API angelegter Benutzer **kann sich nicht anmelden**: Rocket.Chat antwortet
mit `totp-required` und schickt einen Code per E-Mail. Ursache ist
`Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In` (in der Testinstanz aktiv) — jeder
neue Benutzer bekommt E-Mail-2FA automatisch.
Geprüft und ausgeschlossen: `users.update` kennt kein Feld dafür („must NOT have
additional properties"), `users.resetTOTP` betrifft nur App-basiertes TOTP.
**Es gibt keinen Weg über die Admin-API, das E-Mail-2FA eines einzelnen Benutzers
abzuschalten.**
Damit stehen drei Wege offen — die Entscheidung gehört dir, weil sie eine
Sicherheitseinstellung berührt:
| Weg | Ablauf | Preis |
|---|---|---|
| **A (empfohlen)** | `Auto_Opt_In` global auf **aus**, dann Benutzer anlegen (Rollen `bot` + `user`), als dieser anmelden, PAT erzeugen, Passwort verwerfen | Neue **menschliche** Benutzer bekommen E-Mail-2FA dann nicht mehr automatisch. Bestehende Konten und TOTP bleiben unberührt |
| **B** | Setting bleibt; jeder Agent braucht ein echtes Postfach, ClawdDotNet holt den 2FA-Code per IMAP (das Mail-Tool kann das) | Funktioniert, ist aber ein zerbrechlicher Umweg |
| **C** | Halbautomatisch: API legt den Benutzer an, ein Mensch erzeugt das PAT einmalig in der Oberfläche | Kein „HR-Agent" möglich — bei jedem neuen Agenten Handarbeit |
**Weg A ist umgesetzt und gemessen** (August 2026): Nach dem Abschalten von `Auto_Opt_In`
läuft die Kette vollständig durch —
```
users.create (Rollen bot + user) → login als dieser Benutzer →
users.generatePersonalAccessToken → PAT
```
Damit ist ein „HR-Agent", der einen neuen Agenten samt Chat-Konto einrichtet, technisch
möglich. Die Rolle `user` ist dabei nötig, **nicht** nur `bot`: Nur sie bringt die
Berechtigung `create-personal-access-tokens` mit.
Ein Benutzer, der **vor** der Umstellung angelegt wurde, behält sein 2FA-Flag dauerhaft —
er lässt sich nicht nachträglich retten und muss neu angelegt werden.
Ob sich `Auto_Opt_In` nach der Bereitstellung wieder einschalten lässt, ohne die
bestehenden Agenten zu verlieren, ist plausibel (das Flag wird beim Anlegen gesetzt), aber
weiterhin **nicht gemessen**.
Für das Tool selbst ist die Frage folgenlos: Es nimmt `userId` und `authToken` aus der
Konfiguration entgegen, gleich woher sie stammen.
### 2.0.2 Zwei Fehler, die erst der Live-Test zeigte
Beide wären in keinem Schreibtischtest aufgefallen und sind behoben:
1. **`unread` zählt in dieser Instanz nur Erwähnungen.** Eine gewöhnliche Nachricht setzt
allein `alert=true`. Schwerer wiegt: `userMentions` ist ein Zähler über *ungelesene*
Erwähnungen und bleibt stehen, solange nichts gelesen wurde. Ein einziger alter,
ungelesener Ruf machte den Raum damit dauerhaft „heiß" — und anschließend wurde **jede**
weitere Nachricht ausgeliefert, auch ohne Erwähnung.
*Behoben:* Die Erwähnungsprüfung sitzt jetzt an der einzelnen Nachricht; der
Raum-Zähler ist nur noch ein billiger Vorfilter. Zusätzlich wird jeder geprüfte Raum als
gelesen markiert, auch wenn nichts zu wecken war — sonst veralten die Zähler.
2. **Die Startmarke verschluckte die erste echte Nachricht.** Ein Raum wird erst dann zum
Kandidaten, wenn Verkehr da ist — genau dann setzte der alte Code aber „Marke auf jetzt,
nichts wecken". Die auslösende Nachricht ging verloren.
*Behoben:* Beim ersten Abruf eines Raums wird begrenzt zurückgeschaut
(`initialLookbackMinutes`, Standard 5) statt zu überspringen.
### 2.1 Identität — ein echter Benutzer je Agent
Die Anforderung „jeder Agent mit eigenem Benutzer, in Gruppen und im Direktkontakt" ist
@@ -198,27 +296,143 @@ ist das am Ende nicht optional.
### 2.8 Tool-Zuschnitt
**Umgesetzt** (`src/ClawdDotNet.Tools.RocketChat`, gegen die Testinstanz geprüft):
```
Tool: RocketChat
Aktionen: send_message | reply | send_file | list_rooms | read_room
| mark_read | search
Aktionen: send_message | reply | send_file | list_rooms | read_room | mark_read
Job: rocketchat_poll
```
Noch nicht umgesetzt: `search`.
**Dateiversand.** `send_file` schickt eine Datei aus dem Workspace direkt in einen Raum
oder als Direktnachricht — der kurze Weg für „stell mir das zusammen und schick es rüber",
ohne Umweg über die Cloud. Pfade tragen dieselben Prefixe wie bei FileRW und FTP
(`personal:` / `shared:` / ohne Prefix), samt Prüfung gegen einen Ausbruch aus dem
Verzeichnis. Grenze über `maxUploadMb` (Standard 25, Serverseite erlaubt 100).
Dabei zeigte sich die **dritte Doku-Korrektur**: Der Ein-Schritt-Endpunkt `rooms.upload`
existiert in 8.7 nicht mehr — er antwortet mit einem nackten HTML-404. Aktuell ist ein
zweistufiger Ablauf:
1. `rooms.media/:rid` nimmt die Datei als `multipart/form-data` und liefert eine Datei-Id;
sichtbar ist damit noch **nichts**.
2. `rooms.mediaConfirm/:rid/:fileId` veröffentlicht sie als Nachricht.
Ohne den zweiten Schritt liegt die Datei hochgeladen, aber unsichtbar auf dem Server.
Voraussetzung ist damit Rocket.Chat 6.x oder neuer; einen Rückfall auf `rooms.upload` für
ältere Instanzen gibt es bewusst nicht, weil er sich hier nicht prüfen ließe.
Anders als `send_message` steht `RocketChat.send_file` in der Staging-Policy auf
**`approve`**: Eine Nachricht formuliert der Agent, eine Datei verlässt den Workspace als
Ganzes. Wer das im Alltag als zu hinderlich empfindet, streicht die eine Zeile in
`StagingPolicy.DefaultRules` — dann schützt weiterhin die Raum-Allowlist.
Geprüft gegen die Testinstanz (8 von 8): Markdown aus dem persönlichen Workspace · CSV aus
dem geteilten mit abweichendem Dateinamen · Datei per Direktnachricht · Ausbruchsversuch
`../` abgewiesen · absoluter Pfad abgewiesen · fehlende Datei · fehlender `localPath` ·
nicht freigegebener Raum.
### 2.9 Eingehende Dateien und Links
Was hereinkommt, ist genauso wichtig wie das, was hinausgeht — und stand anfangs nicht im
Entwurf.
**Dateien.** Eine Dateisendung trägt den Begleittext in `msg`, die Datei selbst aber
daneben in `files[]`. Wer nur `msg` liest, sieht bei einer reinen Dateisendung einen
**leeren Beitrag**. Weckmeldung und `read_room` führen Anhänge deshalb eigens auf:
```
[20:06] @richard (messageId: rD68…): schau dir die Zahlen bitte an.
Datei: auswertung.csv (text/csv, 77 B) — fileId: 6a821828ea0ad1bcab878f74
```
Mit dieser `fileId` holt `download_file` die Datei in den Workspace. Der Abruf geht gegen
`/file-upload/{fileId}/download` mit denselben Kopfzeilen wie die API — ohne sie antwortet
der Server mit 403 (`FileUpload_ProtectFiles` ist aktiv).
Schutzmaßnahmen, weil Name und Inhalt vom Absender bestimmt sind:
- Der Zielname wird auf den reinen Dateinamen reduziert; Pfadangaben darin verfallen.
Nur das Verzeichnis (`personal:` / `shared:`) darf der Agent wählen.
- Ausführbare Endungen (`.exe`, `.ps1`, `.bat`, `.jar`, …) werden abgelehnt, sofern nicht
`allowDangerousDownloads` gesetzt ist.
- Größengrenze `maxDownloadMb` (Standard 25) — geprüft an `Content-Length` **und**
während des Schreibens, da die Angabe fehlen darf.
- Geschrieben wird über eine `.part`-Nebendatei; bricht der Abruf ab, bleibt keine halbe
Datei liegen, die der Agent für vollständig hält.
Dabei fiel ein Windows-Fehler auf, der leicht zu übersehen ist: `Path.GetFileName` behält
`"shared:kopie.csv"` unverändert bei, weil `:` dort kein Pfadtrenner ist, sondern ein
**NTFS-Alternativdatenstrom** eingeleitet wird. Die Datei landete als Datenstrom am
Verzeichnis statt als eigene Datei. Der Präfix wird jetzt vor der Namensbereinigung
abgetrennt, und `WorkspaceFile` weist Doppelpunkte grundsätzlich ab.
**Links.** Rocket.Chat entpackt Links selbst und legt in `urls[].meta` Titel und
Beschreibung der Zielseite ab. Der Agent bekommt das mitgeliefert:
```
Link: https://www.rocket.chat/ — Rocket.Chat | Secure CommsOS™ …
```
Das erspart oft einen eigenen Seitenabruf. **Wichtig:** Diese Vorschau ist Text der
verlinkten Seite, also fremdbestimmt — sie steht deshalb wie alles andere innerhalb der
`<untrusted_content>`-Rahmung. Ein tatsächlicher Abruf der Seite bleibt Sache des
WebFetch-Tools und damit eine bewusste Entscheidung des Agenten, keine Nebenwirkung des
Empfangens.
Geprüft (9 von 9): Datei in der Weckmeldung samt `fileId` · Link mit Titel · Download in
den persönlichen und in den geteilten Workspace · Inhalt stimmt · Ausbruch über den
Dateinamen entschärft · gefährliche Endung abgelehnt · unbekannte `fileId` · fehlende
`fileId`.
Der Weckpfad ist gegen die Testinstanz durchgespielt (6 von 6 Erwartungen):
ruhiger Takt weckt nicht · Nachricht ohne Erwähnung weckt nicht · Erwähnung weckt ·
Direktnachricht weckt auch ohne Erwähnung · eigene Nachricht weckt nicht ·
private Gruppe weckt.
Konfiguration je Agent:
```json
"RocketChat": {
"baseUrl": "https://chat.example.org",
"userId": "aBcD…",
"authToken": "…", // von ConfigSecrets geschützt
"allowedRooms": ["GENERAL", "finanz-team"],
"authToken": "…", // von ConfigSecrets geschützt (Schlüssel "authtoken")
"username": "agent-hermes", // für den Erwähnungsfilter
"allowedRooms": ["GENERAL", "finanz-team", "richard"],
"defaultRoom": "finanz-team",
"mentionOnly": true,
"maxWakesPerRoomPerHour": 12
"agentUsernames": ["agent-hermes", "agent-atlas"],
"maxWakesPerRoomPerHour": 12,
"maxMessagesPerRoom": 20,
"initialLookbackMinutes": 5,
"maxUploadMb": 25
}
```
**`allowedRooms` gilt auch für Direktnachrichten.** Ein DM-Raum trägt den Namen des
Gegenübers — wer per DM erreichbar sein soll, steht dort mit seinem **Benutzernamen**
(oben `"richard"`). Ohne Eintrag ignoriert der Agent die Direktnachricht. Eine leere Liste
erlaubt alles; das Tool erfindet keine Allowlist.
Mit `"@benutzername"` als `room` beginnt der Agent auch ein **neues** Gespräch (`im.create`)
— nur bei ausdrücklicher `@`-Schreibweise, damit ein vertippter Kanalname nicht
stillschweigend zur Direktnachricht wird.
Zum Verhalten des Abrufs:
- **Erster Takt je Raum setzt nur die Marke** und weckt nicht — sonst käme beim Einrichten
die gesamte Raumgeschichte auf einmal in den Kontext.
- Die Marke wandert auf die jüngste **gesehene** Nachricht, auch auf gefilterte. Sonst
würde eine ignorierte Agentennachricht bei jedem Takt erneut geprüft.
- **Gelesen-Markierung erst nach dem Einsammeln** — bricht der Takt vorher ab, bleibt der
Zähler stehen und nichts geht verloren.
- Der Schleifenschutz vergleicht gegen `agentUsernames`, nicht gegen ein Server-Flag: Wir
wissen selbst am besten, welche Konten unsere Agenten sind.
- **Kein Eintrag in der Staging-Policy** — `RocketChat.send_message` bleibt bewusst `auto`
(Standard). Der Schutz sitzt an `allowedRooms`, nicht an einer Einzelfreigabe.
---
## 3 — Redundanz: was passiert, wenn Rocket.Chat ausfällt