# Rocket.Chat und Nextcloud — Konzept

Zwei neue Tools, ein gemeinsamer Zweck: **Rocket.Chat** wird der Ort, an dem wir mit den
Agenten reden; **Nextcloud** wird der Ort, an dem die Agenten uns Ergebnisse hinlegen.
Der typische Ablauf ist die Kombination aus beidem — „schreib mir die Auswertung und leg
sie in die Cloud" im Chat, Datei in Nextcloud, Link zurück in den Chat.

Dieses Dokument prüft die Machbarkeit, legt den Schnitt fest und benennt die Punkte, die
vor der Umsetzung entschieden werden müssen.

> **Stand 2026-08-23:** Der Rocket.Chat-Teil ist **umgesetzt** —
> `src/ClawdDotNet.Tools.RocketChat`, im `AppHost` registriert, `send_file`
> freigabepflichtig. Der Befund unten bleibt als Begründung stehen; Abschnitt 2.0
> hält fest, was die Messung gegen die echte Instanz an den Annahmen korrigiert hat.
> **Offen:** der Nextcloud-Teil und die Kollision mit Roadmap A5 (Matrix) — die ist
> eine Entscheidung, keine Umsetzung.

Verwandt: [Taskboard-Konzept](Taskboard-Konzept.md) (Scanner/Wake), [Staging-Konzept](Staging-Konzept.md)
(Freigaben), [Audit-Konzept](Audit-Konzept.md), [Roadmap](Roadmap.md) (A5 — siehe Konflikt unten).

---

## 0 — Kurzfassung des Befunds

| Frage | Antwort |
|---|---|
| Ist es umsetzbar? | Ja, beides. Ohne neue Architektur — die vorhandenen Bausteine tragen. |
| Braucht es Änderungen am Core? | Für Phase 1: **nein**, nur zwei neue Tool-Projekte + Staging-Defaults. Für den automatischen Rückweg (Antwort landet ohne Zutun des Modells im Raum) und den Notfallkanal: ja, zwei kleine Core-Ergänzungen. |
| Größtes technisches Risiko | Nicht die API — sondern **Antwort-Schleifen zwischen Agenten** und **Kosten durch zu häufiges Wecken**. |
| Größte Konzeptkollision | Roadmap **A5** sieht Matrix/Element für genau diesen Zweck vor. Rocket.Chat ersetzt A5, oder wir haben zwei Chat-Wege. Muss entschieden werden. |
| „Agent erstellt Dokument direkt über die Nextcloud-API" | So nicht. Nextcloud hat keine API, die Inhalte *erzeugt*. Der Weg ist: Datei lokal im Workspace erzeugen → hochladen. Für PDF/XLSX kann **Collabora als Konverter** dienen — das ist der elegante Teil, siehe 5.4. |

---

## 1 — Was schon da ist (und deshalb nicht neu gebaut wird)

Der Rückkanal von außen nach innen existiert vollständig:

```
TaskScanner (60-s-Takt)
   └─ Task vom Typ tool_job
        └─ EngineTaskDispatcher.DispatchToolJobAsync
             └─ IToolJobProvider.ExecuteJobAsync   ← kein LLM, kostenlos
                  └─ ToolJobResult.Wake(text)      ← nur wenn wirklich etwas da ist
                       └─ AgentEngine.ChatAsync    ← hier erst kostet es Tokens
```

Das Telegram-Tool nutzt genau das (`telegram_poll`). **Rocket.Chat bekommt dieselbe
Bauform** — `rocketchat_poll`. Damit gilt automatisch:

- Zustand (letzter gesehener Zeitpunkt) über `IStateStore`, überlebt Neustarts.
- Ein Takt ohne neue Nachricht kostet nichts.
- Kein eigener Thread, kein eigener Scheduler, keine Sonderbehandlung beim Start.
- Jeder Tool-Aufruf läuft ohnehin durch `StagingGate` (A2) und Audit (A3).

Ebenso vorhanden und wiederverwendbar:

- **Pro-Agent-Konfiguration** (`AgentConfig.Tools["RocketChat"]`) — jeder Agent bekommt
  seine eigenen Zugangsdaten, ohne dass ein Agent die eines anderen sehen kann.
- **`ConfigSecrets`** verschlüsselt Felder nach Namen (`token`, `password`, `apikey` …) —
  ein Feld namens `authToken` bzw. `appPassword` ist automatisch geschützt.
- **Workspace-Prefixe** `personal:` / `shared:` samt Path-Traversal-Prüfung — aus dem
  FTP-Tool wortgleich übernehmbar für Nextcloud-Uploads.

---

## 2 — Rocket.Chat: Machbarkeit

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
der richtige Ansatz und wird von Rocket.Chat direkt unterstützt.

- Admin legt je Agent einen Benutzer an: `POST /api/v1/users.create`
  (`{ name, username, email, password, roles: ["bot"] }`).
- Die Rolle **`bot`** ist wichtig: Sie markiert den Benutzer als Maschine (relevant für
  Schleifenschutz, siehe 2.5) und wird in neueren Versionen bei der Sitzplatzzählung
  nicht als normaler Nutzer gewertet. *Gegen die eigene Version zu prüfen.*
- Für jeden Agenten wird ein **Personal Access Token** erzeugt
  (`POST /api/v1/users.generatePersonalAccessToken`, oder im Konto des Benutzers).
  Dauerhaft gültig, einzeln widerrufbar — deutlich besser als Login mit Passwort, weil
  kein Session-Ablauf und keine gespeicherten Passwörter im Spiel sind.
- Authentifiziert wird jeder Aufruf über zwei Header: `X-Auth-Token` und `X-User-Id`.

**Entscheidung, die ich empfehle:** Das Anlegen der Benutzer ist **kein Agenten-Tool**.
Es ist eine einmalige Einrichtungsfunktion in der Oberfläche
(Instanz-Einstellungen → Rocket.Chat → „Agenten-Benutzer anlegen"). Sonst müsste ein
Agent ein Admin-Token halten — und ein Admin-Token in Reichweite einer Prompt-Injection
ist genau das, was A2 verhindern soll. Der Admin-Token liegt in der **Instanz**-Konfiguration,
nicht in einer Agenten-Tool-Konfiguration.

### 2.2 Ausgang — Nachrichten senden

| Zweck | Endpunkt |
|---|---|
| In Kanal/Gruppe/DM schreiben | `POST /api/v1/chat.postMessage` (`roomId` oder `channel`) |
| Auf eine Nachricht antworten (Thread) | dasselbe, mit `tmid` |
| Datei anhängen | `POST /api/v1/rooms.upload/{roomId}` (multipart) |
| Reaktion setzen | `POST /api/v1/chat.react` |

Gesendet wird als der Agenten-Benutzer — Direktnachrichten funktionieren dadurch echt und
nicht als „Bot mit Alias".

### 2.3 Eingang — der Poll-Weg (Phase 1)

Der sparsame Weg, ohne jede neue Infrastruktur:

1. `GET /api/v1/subscriptions.get?updatedSince=<zeitstempel>` — **ein einziger Aufruf**
   liefert für diesen Agenten alle Räume mit Ungelesen-Zähler, Erwähnungs-Zähler und
   „zuletzt gesehen"-Marke. Auch bei 50 Räumen bleibt es ein Aufruf.
2. Nur für Räume mit relevanten Neuigkeiten wird die Historie geholt:
   `channels.history` (öffentlich) / `groups.history` (privat) / `im.history` (DM),
   jeweils mit `oldest=<letzte gesehene Zeit>`.
3. `POST /api/v1/subscriptions.read` markiert gelesen — der Zähler geht zurück auf null.

Zustand im `IStateStore`: `rocketchat:{agentId}:lastCheck` sowie je Raum die zuletzt
verarbeitete Nachrichtenzeit.

**Rate-Limits:** Rocket.Chat begrenzt REST-Aufrufe (Standard in der Größenordnung von
10 Aufrufen je Minute und Endpunkt). Bei einem Takt von 30–60 Sekunden und einem
Sammelaufruf pro Takt ist das unkritisch — es ist aber der Grund, warum der Entwurf über
`subscriptions.get` sammelt statt jeden Raum einzeln zu pollen.

**Latenz:** Bei 60-Sekunden-Takt antwortet ein Agent im Mittel nach ~30 s plus Laufzeit.
Für Gespräche mit Agenten ist das spürbar, aber tragbar. Der Takt lässt sich pro Job
setzen (`*/1 * * * *` ist das Minimum des Cron-Modells; feiner ginge nur über die
Realtime-API).

### 2.4 Eingang — die Realtime-Variante (Phase 3, optional)

Rocket.Chat bietet eine WebSocket-/DDP-Schnittstelle (`wss://host/websocket`): nach
`login` mit dem Token abonniert man `stream-notify-user/{userId}/notification` und
bekommt DMs und Erwähnungen **sofort** gepusht, ohne Polling.

Das ist die richtige Endstufe (Antwortzeit ~1 s statt ~30 s), aber es ist eine dauerhafte
Verbindung je Agent mit Wiederverbindungs-Logik — also eine echte Komponente, keine
Ergänzung eines Tools. Vorschlag: **erst nachrüsten, wenn Phase 1 im Alltag steht** und
sich die Verzögerung tatsächlich stört.

Eine dritte Möglichkeit — Rocket.Chats *Outgoing Webhook* auf unsere vorhandene
`ClawdDotNetApi` (Port 5082) — wäre die einfachste Push-Lösung, setzt aber voraus, dass
der Rocket.Chat-Server den Windows-Rechner über das Netz erreicht. Das ist eine Frage
deiner Netztopologie und keine der Software. Falls erreichbar: der kürzeste Weg zu
niedriger Latenz.

### 2.5 Die zwei echten Fallen

Diese beiden Punkte sind wichtiger als jede API-Frage.

**(a) Mehrere Agenten im selben Raum.** Wenn drei Agenten denselben Gruppenchat pollen,
antworten drei Agenten auf jede Nachricht. Regel im Entwurf:

> Ein Agent wird nur geweckt bei (1) Direktnachrichten an ihn oder (2) Nachrichten, die
> ihn per `@name` erwähnen. Alles andere liest er nicht einmal.

Ein Raum kann per Konfiguration auf `respondToAll: true` gestellt werden — das ist die
bewusste Ausnahme für einen Raum mit genau einem Agenten.

**(b) Agenten-Schleifen.** Agent A schreibt, Agent B wird geweckt, antwortet, weckt A —
und das läuft, bis das Tagesbudget greift. Der `LoopGuard` schützt nur *innerhalb* eines
Laufs, nicht über Agenten hinweg. Regel im Entwurf:

> Nachrichten von Benutzern mit der Rolle `bot` werden **ignoriert**, außer der Agent ist
> namentlich erwähnt. Zusätzlich eine Drossel: höchstens N Weckvorgänge je Raum und
> Stunde (Zähler im `IStateStore`), danach schweigt der Agent in diesem Raum bis zur
> nächsten Stunde und protokolliert das.

Das Tagesbudget (K5) ist das letzte Netz, nicht das erste.

### 2.6 Der Rückweg der Antwort

Der Wake-Mechanismus liefert die Nachricht *in* den Agenten. Seine Antwort geht heute in
den Chat-Verlauf, nicht zurück nach Rocket.Chat. Zwei Wege:

- **(a) Der Agent antwortet selbst** — die Weck-Nachricht enthält die `roomId` und die
  Anweisung, mit `RocketChat.send_message` zu antworten. Kein Core-Eingriff, funktioniert
  sofort. Schwäche: Es hängt daran, dass das Modell es tut. Erfahrungsgemäß klappt das
  gut, aber nicht in 100 % der Fälle.
- **(b) Automatischer Rückweg** — der Tool-Job merkt sich „Antwort gehört nach Raum X",
  und der Dispatcher schickt die Abschlussnachricht des Laufs dorthin. Zuverlässig, aber
  es braucht einen kleinen Haken in `ToolJobResult`/`EngineTaskDispatcher`
  (etwa ein `ReplyTo`-Feld, das der Dispatcher nach dem Lauf an dasselbe Tool zurückgibt).

**Empfehlung:** (a) in Phase 1, (b) in Phase 2 nachziehen — denn (b) ist der Unterschied
zwischen „meistens antwortet er" und „er antwortet". Für die Hauptkommunikationsschiene
ist das am Ende nicht optional.

### 2.7 Sicherheit

- **Nachrichten aus Rocket.Chat sind fremder Text.** Sie müssen als
  `<untrusted_content>` gerahmt in den Kontext (Roadmap K2-Rest). Bei Telegram fehlt das
  bis heute; hier sollte es von Anfang an drin sein, weil Gruppenchats mehrere Absender
  haben.
- **Raum-Allowlist** je Agent (`allowedRooms`), analog `allowedChatIds` beim Telegram-Tool.
- **Staging-Vorschlag** (siehe 6): Senden in erlaubte Räume `auto`, alles darüber hinaus
  `approve`.
- Zugangsdaten heißen im Konfigurationsfeld `authToken` → `ConfigSecrets` verschlüsselt sie
  automatisch. Der Admin-Token der Instanz muss in `ConfigSecrets.Apply(InstanceConfig)`
  ergänzt werden.
- **TLS** ist Pflicht; selbstsignierte Zertifikate ausdrücklich konfigurieren müssen statt
  Validierung generell abschalten.

### 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
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 (Schlüssel "authtoken")
  "username": "agent-hermes",    // für den Erwähnungsfilter
  "allowedRooms": ["GENERAL", "finanz-team", "richard"],
  "defaultRoom": "finanz-team",
  "mentionOnly": true,
  "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

Das ist die Anforderung, die die Architektur bestimmt — nicht der Chat selbst. Der Kern:
**Rocket.Chat darf ein Kanal sein, nicht der Kanal.**

### 3.1 Was heute schon unabhängig funktioniert

| Kanal | Unabhängig von Rocket.Chat? | Richtung |
|---|---|---|
| Chat-Seite in der App (`ChatPageView`) | vollständig — läuft in der App selbst | beide |
| Telegram-Bot-Tool | ja — fremde Infrastruktur | beide |
| Mail-Tool | ja, sofern der Mailserver anderswo läuft | beide |
| Web-Chat / `ClawdDotNetApi` | ja, aber nur im lokalen Netz | beide |

Wir sind also nicht bei null. Was fehlt, ist die **Umschaltung** — heute muss ein Mensch
merken, dass nichts mehr ankommt.

### 3.2 Vorschlag: `ChannelRouter` im Core

Eine kleine Komponente im Core (kein neues Tool, keine Tool-zu-Tool-Abhängigkeit —
sie löst Tools über die vorhandene `ToolRegistry` nach Namen auf, wie es der Dispatcher
schon tut):

- Je Agent eine **geordnete Kanalliste**, z. B. `["RocketChat", "Telegram", "Mail"]`.
- Eine Methode „stelle dem Menschen diese Nachricht zu": versucht der Reihe nach, bis
  einer erfolgreich ist, und protokolliert im Audit-Log, **über welchen Kanal** zugestellt
  wurde — inklusive des Hinweises „Primärkanal war nicht erreichbar".
- Genutzt von: Agenten (`notify_user`), aber vor allem von **systemseitigen** Meldungen,
  die heute keinen Weg nach außen haben: Staging-Vorschlag wartet auf Freigabe, Budget
  überschritten, Watchdog-Alarm, Task blockiert.

Der zweite Teil ist der wichtigere: Gerade wenn etwas kaputt ist, ist die Meldung darüber
diejenige, die ankommen muss.

### 3.3 Gesundheitsprüfung und Eskalation

Ein Tool-Job `rocketchat_health` (Takt ~5 Minuten, `GET /api/info`):

- Nach **drei** aufeinanderfolgenden Fehlschlägen: einmalige Meldung über den nächsten
  Kanal der Liste — „Rocket.Chat ist seit HH:MM nicht erreichbar, ich melde mich hier."
  Einmalig, nicht je Takt.
- Bei Rückkehr: „Rocket.Chat ist wieder da", und der Zustand wird zurückgesetzt.
- Nachrichten, die während des Ausfalls nicht gesendet werden konnten, werden **nicht**
  in einer eigenen Warteschlange gehalten — sie gehen über den Ersatzkanal raus. Eine
  zweite Zustellwarteschlange wäre eine zweite Fehlerquelle.

**Eingehend während des Ausfalls:** Der Telegram-Poll-Job bleibt dauerhaft aktiv, nur mit
langsamem Takt (z. B. alle 5 Minuten). Er kostet nichts, wenn nichts kommt — und ist im
Ernstfall der Weg, auf dem *du* die Agenten erreichst. Die eingebaute Chat-Seite ist ohnehin immer
da, solange die App läuft.

**Entschieden (August 2026): Telegram ist der Notfallkanal.** Die Kanalliste lautet damit
`["RocketChat", "Telegram"]`. Konsequenzen:

- Das Telegram-Tool wird **nicht** abgebaut und geht nicht in Rocket.Chat auf. Es behält
  seine Rolle, verliert aber die Rolle als Alltagskanal.
- `Telegram.send_message` bleibt in der Staging-Policy auf `approve` — mit einer Ausnahme:
  Meldungen, die der `ChannelRouter` selbst erzeugt (Ausfall, Budget, Watchdog, offene
  Freigabe), laufen **ohne** Freigabe. Sonst bliebe die Warnung, dass eine Freigabe
  aussteht, selbst in der Freigabewarteschlange hängen — ein Ringschluss, der genau im
  Ernstfall zuschlägt.
- Der Telegram-Poll bleibt dauerhaft eingerichtet, aber mit langsamem Takt. Ein Kanal, der
  erst im Notfall eingeschaltet wird, ist im Notfall ungetestet.
- Mail bleibt außen vor. Zwei Ersatzkanäle zu pflegen lohnt nicht; das Mail-Tool behält
  seinen fachlichen Zweck.

### 3.4 Was das für die Prompts heißt

Ein Agent soll seinen Kanal nicht selbst wählen. Er sagt „ich möchte dem Nutzer das hier
mitteilen", der Router entscheidet. Sonst muss das Modell im Fehlerfall improvisieren —
und genau dann ist Improvisation das Letzte, was man will.

---

## 4 — Konflikt mit Roadmap A5 (Matrix)

Roadmap-Punkt **A5** legt fest: „Die Kommunikation (Benachrichtigungen, Berichte, Chat mit
Agenten) wird auf Element/Matrix umgestellt", und der gestrichene Tool-Kandidat *Notify*
geht darin auf.

Rocket.Chat besetzt exakt dieselbe Rolle. Drei mögliche Auflösungen:

1. **Rocket.Chat ersetzt A5.** A5 wird umgeschrieben, Matrix entfällt. Vorteil: eine
   Schiene, ein Betriebsaufwand, die Instanz läuft bereits.
2. **A5 bleibt, Rocket.Chat ist nur ein weiteres Tool.** Dann bauen wir zweimal dasselbe.
   Schwer zu begründen.
3. **Rocket.Chat primär, Matrix als späterer Zweitkanal.** Passt formal zur
   Redundanz-Anforderung, verdoppelt aber den Wartungsaufwand für einen Fall, den
   Telegram schon abdeckt.

**Meine Empfehlung: (1).** Der `ChannelRouter` aus 3.2 ist ohnehin die Verallgemeinerung,
die A5 gebraucht hätte — mit ihm ist ein späterer Matrix-Kanal ein zusätzlicher Eintrag in
der Liste, keine Migration. Das ist eine Entscheidung für dich, keine technische Sachfrage.

---

## 5 — Nextcloud: Machbarkeit

### 5.1 Der Zugriffsweg

Nextcloud hat zwei Schnittstellen, beide brauchen wir:

| Zweck | Schnittstelle |
|---|---|
| Dateien lesen/schreiben/auflisten/verschieben | **WebDAV**: `/remote.php/dav/files/{benutzer}/{pfad}` |
| Öffentlichen Link erzeugen | **OCS**: `/ocs/v2.php/apps/files_sharing/api/v1/shares` |

Authentifiziert wird mit **App-Passwörtern** (Nextcloud → Einstellungen → Sicherheit →
„Neues App-Passwort erstellen") per Basic-Auth. Ein App-Passwort ist einzeln widerrufbar
und lässt das eigentliche Kontopasswort unangetastet — dieselbe Logik wie das Personal
Access Token bei Rocket.Chat.

WebDAV braucht keine Bibliothek: `HttpClient` mit den Methoden `PUT`, `GET`, `MKCOL`,
`PROPFIND`, `MOVE`, `DELETE`. Nur `PROPFIND` liefert XML (Multistatus), das geparst werden
muss — überschaubar, und es erspart uns eine weitere Abhängigkeit.

### 5.2 Ein Benutzer je Agent — oder ein Sammelkonto?

Zwei Modelle:

- **Je Agent ein Nextcloud-Benutzer.** Sauber nachvollziehbar („wer hat das abgelegt"),
  passt zum Rocket.Chat-Modell, kostet je nach Lizenzmodell Nutzer.
- **Ein Dienstkonto `clawd-agents` mit Unterordnern je Agent.** Einfacher zu verwalten,
  Herkunft steht dann im Pfad statt im Konto.

**Empfehlung:** Ein Dienstkonto mit Ordnerstruktur `/ClawdDotNet/{Agent}/…`, **plus** einen
mit dir geteilten Ordner `/ClawdDotNet/Berichte/`. Begründung: Bei Rocket.Chat ist die
eigene Identität funktional zwingend (DMs, Erwähnungen), bei Dateien ist sie es nicht —
und ein Ordnerbaum ist leichter aufzuräumen als zehn Konten. Falls du die Trennung dennoch
willst, ändert das am Tool nichts, nur an der Konfiguration.

Die Ordnerdurchsetzung gehört ins Tool: eine konfigurierte `rootPath`, aus der der Agent
nicht ausbrechen kann — dieselbe Prüfung wie in `FTPTool.ResolveLocalPath`.

### 5.3 Die ehrliche Antwort zu „direkt über die API erstellen"

Nextcloud hat **keine** API, die Dokumenteninhalte erzeugt. Es ist ein Dateiablage- und
Freigabesystem; Collabora ist ein *Editor* im Browser (über WOPI angebunden), kein
Generator, den man von außen mit „erstelle eine Tabelle mit diesen Zahlen" beauftragen
kann.

Der tatsächliche Weg ist deshalb der, den du selbst schon beschrieben hast:

```
Agent erzeugt die Datei im eigenen Workspace   (FileRW-Tool, schon vorhanden)
   → Nextcloud.upload                          (WebDAV PUT)
   → Nextcloud.share  (optional)               (OCS, liefert Link)
   → RocketChat.send_message mit dem Link
```

Das ist kein Umweg, sondern die richtige Aufteilung: Der Agent kann seine Datei lokal
prüfen und korrigieren, bevor sie irgendwo landet.

### 5.4 Formate — und wo Collabora doch nützlich wird

Was ein Agent von sich aus gut schreiben kann: **Markdown** (Nextcloud rendert `.md`
direkt in der Weboberfläche — für Berichte oft die beste Wahl), **CSV**, **HTML**, JSON.

Was er nicht von sich aus schreiben kann: `.xlsx`, `.docx`, `.pdf`.

Hier gibt es einen eleganten Weg, weil du Collabora ohnehin betreibst: Collabora Online
bringt einen **Konvertierungs-Endpunkt** mit (`POST /cool/convert-to/{format}`, multipart).
Damit gilt:

| Ziel | Weg |
|---|---|
| PDF | Agent schreibt HTML oder ODT → Collabora → PDF |
| XLSX | Agent schreibt CSV → Collabora → XLSX |
| DOCX | Agent schreibt HTML/ODT → Collabora → DOCX |

Vorteil: **keine zusätzliche PDF- oder Excel-Bibliothek** im Projekt (und keine
Lizenzfrage, die wir uns damit einhandeln — mehrere verbreitete .NET-Bibliotheken für
XLSX und PDF sind für kommerzielle Nutzung nicht frei).

Zu prüfen, bevor wir darauf bauen:
- Ist der Endpunkt in deiner Collabora-Installation erreichbar? Er muss in `coolwsd.xml`
  für die IP des ClawdDotNet-Rechners freigegeben sein (`net`/`post_allow`-Allowlist).
  Standardmäßig ist das eng gefasst.
- Der Pfad heißt je nach Version `/cool/convert-to/…` (neu) oder `/lool/convert-to/…` (alt).

Falls der Endpunkt nicht freigegeben werden soll: Rückfallebene ist Markdown/CSV — für
den Alltag völlig ausreichend, PDF wäre dann ein späterer eigener Punkt.

### 5.5 Freigabe-Links

`POST /ocs/v2.php/apps/files_sharing/api/v1/shares` (Header `OCS-APIRequest: true`),
`shareType=3` = öffentlicher Link. Optional `password`, `expireDate`, `permissions=1`
(nur lesen). Die Antwort enthält die fertige URL.

Zwei Hinweise:
- Manche Instanzen erzwingen Passwortschutz für öffentliche Links — dann muss das Tool ein
  Passwort mitgeben und zurückliefern.
- Ein öffentlicher Link ist **irreversibel im Sinne von A2**: Einmal geteilt, kann er
  weitergegeben worden sein, auch wenn man ihn danach löscht. Deshalb steht er unten in
  der Staging-Tabelle auf `approve`.

Innerhalb der eigenen Instanz ist die freundlichere Variante `shareType=0` (an einen
konkreten Nextcloud-Benutzer) — kein öffentlicher Link nötig, wenn du ohnehin ein Konto
hast. Das sollte der **Standard** sein, öffentlich die Ausnahme.

### 5.6 Fallstricke

- **Dateisperren (HTTP 423).** Wenn du eine Datei gerade in Collabora offen hast, kann ein
  Upload auf dieselbe Datei scheitern. Das Tool muss 423 sauber melden statt kryptisch zu
  scheitern — und beim Überschreiben eines Berichts lieber einen neuen Dateinamen mit
  Zeitstempel vergeben.
- **Überschreiben ist nicht destruktiv**, solange die Versionierung aktiv ist (Nextcloud
  legt automatisch eine Vorversion an). Das ist der Grund, warum `Nextcloud.upload` unten
  auf `auto` steht, `FTP.upload` aber auf `approve`.
- **Größenbegrenzung.** Ein einfaches `PUT` reicht für Berichte problemlos; erst bei sehr
  großen Dateien bräuchte es den Chunked-Upload (`/remote.php/dav/uploads/…`). Für den
  angedachten Zweck (Berichte, Tabellen, PDFs) nicht nötig — und wenn doch, meldet der
  Server einen klaren Fehler.
- **Quota.** Ein Agent, der stündlich Berichte ablegt, füllt das Konto. Ein Aufräum-Task
  („Berichte älter als 90 Tage") gehört mittelfristig ins Taskboard.

### 5.7 Tool-Zuschnitt

```
Tool: Nextcloud
Aktionen: upload | download | list | mkdir | move | delete
          | share | unshare | convert   (convert nur falls Collabora freigegeben)
```

Konfiguration je Agent:

```json
"Nextcloud": {
  "baseUrl": "https://cloud.example.org",
  "username": "clawd-agents",
  "appPassword": "…",             // von ConfigSecrets geschützt
  "rootPath": "/ClawdDotNet/Hermes",
  "allowPublicShares": false,
  "collaboraUrl": "https://collabora.example.org"
}
```

`appPassword` muss der Schlüsselliste in `ConfigSecrets` hinzugefügt werden — `password`
allein greift nicht, weil dort auf ganze Feldnamen verglichen wird.

---

## 6 — Verzahnung mit Staging (A2) und Audit (A3)

Vorschlag für die `StagingPolicy.DefaultRules`:

| Aktion | Standard | Begründung |
|---|---|---|
| `RocketChat.send_message` (erlaubter Raum) | **auto** | Sonst ist Chat unbenutzbar — jede Antwort bräuchte einen Klick |
| `RocketChat.send_message` (Raum nicht in `allowedRooms`) | **deny** | Wird vom Tool selbst abgewiesen, gar nicht erst vorgelegt |
| `RocketChat.send_file` | **approve** | Dateiabfluss in einen Chatraum |
| `Nextcloud.upload`, `mkdir`, `move` | **auto** | Versioniert, im eigenen Ordner, umkehrbar |
| `Nextcloud.delete` | **approve** | wie `FileRW.delete` |
| `Nextcloud.share` (an Benutzer) | **auto** | bleibt innerhalb der Instanz |
| `Nextcloud.share` (öffentlicher Link) | **approve** | nicht zurückholbar |

Der Unterschied zu `Telegram.send_message` (heute `approve`) ist Absicht: Telegram ist ein
Benachrichtigungskanal nach außen, Rocket.Chat ist der Arbeitsraum. Ein Arbeitsraum, in
dem jede Antwort eine Freigabe braucht, ist kein Arbeitsraum. Der Schutz sitzt hier an der
Raum-Allowlist statt an der Einzelfreigabe.

Für das Audit-Log entstehen keine Sonderfälle — die Tool-Aufrufe laufen ohnehin durch.

---

## 7 — Was dieses Konzept **nicht** vorsieht

Damit der Zuschnitt klar ist:

- Keine Rocket.Chat-**App** (Apps-Engine, TypeScript im Server) — wir bleiben Client.
- Keine Verwaltung von Rocket.Chat durch Agenten (Benutzer anlegen, Räume erstellen,
  Rechte vergeben). Das ist Admin-Arbeit in der Oberfläche.
- Keine Sprach-/Videofunktionen, keine Nextcloud Talk-Anbindung.
- Kein Ersatz für die eingebaute Chat-Seite — die bleibt und ist die unterste Rückfallebene.
- Keine Ende-zu-Ende-Verschlüsselung. Rocket.Chat kann das, aber verschlüsselte Räume sind
  über die REST-API nicht lesbar. Agenten arbeiten in unverschlüsselten Räumen — das ist
  eine bewusste Einschränkung, die du kennen solltest.

---

## 8 — Vorschlag für den Schnitt

| Phase | Inhalt | Ergebnis |
|---|---|---|
| **1** | `Nextcloud`-Tool: upload/download/list/mkdir/move/delete/share | Agent kann Berichte ablegen und einen Link liefern |
| **2** | `RocketChat`-Tool: senden, lesen, `rocketchat_poll`-Job, Raum-Allowlist, Erwähnungsfilter, Schleifendrossel | Gespräch mit Agenten über Rocket.Chat, Antwort per Prompt |
| **3** | Automatischer Rückweg (`ReplyTo` in `ToolJobResult`) | Antwort landet zuverlässig im richtigen Raum/Thread |
| **4** | `ChannelRouter` + `rocketchat_health` + Eskalation | Der Notfallkanal — Ausfall wird erkannt und umschifft |
| **5** | Collabora-Konvertierung (PDF/XLSX) | Berichte in Büroformaten |
| **6** *(optional)* | Realtime/DDP statt Polling | Antwortzeit ~1 s statt ~30 s |

Nextcloud zuerst, weil es das kleinere, in sich abgeschlossene Stück ist und sofort Nutzen
bringt — und weil es sich unabhängig vom Ausgang der A5-Entscheidung lohnt.

Phase 4 ist **kein Nice-to-have**: Ohne sie ist Rocket.Chat ein Einzelpunkt, dessen Ausfall
niemand meldet. Sie sollte nicht hinter Phase 5 rutschen.

Zur Modell-Einstufung im Sinne der Roadmap: Phasen 1, 2 und 5 sind klar spezifizierbare
Tool-Arbeit (4.6-tauglich). Phase 3 und 4 fassen Engine bzw. Zustellwege an und sollten
mit vorheriger Festlegung der Invarianten und mit Tests gebaut werden.

---

## 9 — Offene Punkte für die Diskussion

1. **A5/Matrix** — ersetzt Rocket.Chat den Punkt, oder bleibt Matrix als Ziel bestehen?
   (Abschnitt 4; das entscheidet, ob der `ChannelRouter` Pflicht oder Kür ist.)
2. **Nextcloud-Identität** — ein Dienstkonto mit Ordnern je Agent (mein Vorschlag) oder
   je Agent ein eigener Nextcloud-Benutzer?
3. **Rückweg der Antwort** — reicht Phase 2 (Agent antwortet selbst) für den Anfang, oder
   soll Phase 3 direkt mitgebaut werden?
4. ~~**Notfallkanal** — Telegram oder Mail?~~ **Entschieden: Telegram** (siehe 3.3).
   Offen bleibt nur die Kleinigkeit, ob die Reihenfolge instanzweit gilt (mein Vorschlag)
   oder pro Agent einstellbar sein soll.
5. **Collabora-Konvertierung** — ist der `convert-to`-Endpunkt für den ClawdDotNet-Rechner
   freigebbar? Falls nein, bleibt es bei Markdown/CSV.
6. **Versionen** — welche Rocket.Chat- und welche Nextcloud-Version läuft bei dir?
   Einzelne Endpunkte und Rollennamen sind versionsabhängig; das prüfe ich vor der
   Umsetzung gegen deine Instanz statt gegen die Dokumentation.
7. **Öffentliche Links** — grundsätzlich erlauben (mit Freigabe) oder ganz sperren
   (`allowPublicShares: false` als harte Voreinstellung)?
