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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user